Longest Common Subsequence
Даны две строки: text1 и text2. Подпоследовательность строки сохраняет некоторые её буквы в исходном порядке, а остальные пропускает; сохранённые буквы не обязательно должны стоять рядом. Верните длину самой длинной строки, которая является подпоследовательностью обеих строк, или 0, если в этих двух строках нет общих букв.
Функция
- text1string
- первая строка
- text2string
- вторая строка
- Возвращаетinteger
- длина наибольшей общей подпоследовательности
Ограничения
1 ≤ text1.length ≤ 10001 ≤ text2.length ≤ 1000- Обе строки содержат только строчные английские буквы.
Примеры
- Ввод
- text1 = "stone"text2 = "longest"
- Вывод
- 3
- Пояснение
- Буквы o, n, e идут в таком порядке в обоих словах, поэтому
one— общая подпоследовательность длины 3. В словеlongestбуквы s и t стоят в конце, а в словеstone— в начале, поэтому общей подпоследовательностью, в которой они используются, может быть толькоst, что короче.
- Ввод
- text1 = "pear"text2 = "reap"
- Вывод
- 2
- Пояснение
eaвстречается в обоих словах. Буквы p и r находятся по разные стороны отeaв этих двух словах, поэтому ни одна из них не может присоединиться к нему, и ответ — 2.
- Ввод
- text1 = "cat"text2 = "dog"
- Вывод
- 0
- Пояснение
- В этих двух словах нет общих букв, поэтому единственная общая подпоследовательность — пустая, её длина равна 0.
+19 скрытых тестов при отправке
Дополнительный вопрос
Можешь вернуть одну наидлиннейшую общую подпоследовательность, а не только её длину?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Посмотри на последнюю букву каждой строки. Что можно сказать об ответе, когда две буквы совпадают, и что — когда они различаются?
Если буквы совпадают, сопоставь их, а оставшаяся задача для обеих строк будет такой же, но с удалённой этой буквой. Если они различаются, одна из двух как минимум не используется, поэтому попробуй удалить каждую из них и выбери лучший ответ.
Одни и те же пары префиксов встречаются снова и снова. Сохраняй ответ для каждой пары длин префиксов
(i, j)в таблице, начни с пустых префиксов, для которых ответ равен 0, заполняй её строка за строкой и считай ответ из последней ячейки.
Решение
Жадное сопоставление букв не работает. Одна буква может соответствовать многим позициям в другой строке, и первое совпадение может помешать найти лучшие: если сопоставить c из cab с c в конце abc, для a и b ничего не останется, а если пропустить это совпадение, получится ab. Разгадка в том, что ответ для двух префиксов зависит только от ответов для немного более коротких префиксов. Таблица из (n+1) × (m+1) чисел позволяет один раз решить каждую пару, а поскольку каждая строка считывает данные только из строки выше, достаточно двух строк.
Сравнение первых букв с помощью рекурсии
Верно, но не успевает на самых больших тестах
Идея
Пусть lcs(i, j) — это ответ для суффиксов text1[i:] и text2[j:]. Посмотрим на их первые буквы. Если они совпадают, объединим их в пару: самую длинную общую подпоследовательность, в которой эта пара не используется, можно заменить на подпоследовательность с этой парой в начале, не сократив её. Поэтому ответ — 1 + lcs(i+1, j+1).
Если буквы различаются, обе использовать нельзя: каждую из них можно сопоставить только с более поздней буквой другой строки, и пары пересекутся. Поэтому одну из них можно пропустить: ответ — max(lcs(i+1, j), lcs(i, j+1)). Если один из суффиксов пуст, у них нет общих букв, и ответ равен 0.
Алгоритм работает медленно, потому что при каждом несовпадении выполняются два вызова. Если в строках нет общих букв, при каждом вызове возникает несовпадение, пока одна из строк не закончится, а число вызовов растёт примерно как число способов чередовать две строки. Для двух строк из 20 букв это около 2.8 × 10^11 вызовов; большие тесты содержат по 1000 букв в каждой строке. При этом существует всего (n+1) × (m+1) разных пар (i, j), поэтому почти каждый вызов повторяет предыдущий.
Алгоритм
- Запиши
lcs(i, j)для суффиксов, начинающихся сiиj. - Если
iилиjвыходит за конец своей строки, верни 0. - Если
text1[i] == text2[j], верни1 + lcs(i+1, j+1). - Иначе верни
max(lcs(i+1, j), lcs(i, j+1)). - Ответ —
lcs(0, 0).
def longestCommonSubsequence(text1, text2):
def lcs(i, j):
# The longest common subsequence of text1[i:] and text2[j:]
if i == len(text1) or j == len(text2):
return 0
if text1[i] == text2[j]:
return 1 + lcs(i + 1, j + 1)
return max(lcs(i + 1, j), lcs(i, j + 1))
return lcs(0, 0)Заполните таблицу префиксов
Идея
Состояние. Пусть dp[i][j] — наибольшая общая подпоследовательность первых i букв text1 и первых j букв text2. Работа с префиксами позволяет считать, что индекс 0 обозначает пустую строку.
Рекуррентное соотношение. Сравните последние буквы двух префиксов: text1[i-1] и text2[j-1]. Если они равны, сопоставьте их: dp[i][j] = dp[i-1][j-1] + 1. Если нет, пропустите одну из них: dp[i][j] = max(dp[i-1][j], dp[i][j-1]). Это те же рассуждения, что и в рекурсии, только идущие с конца. Базовый случай: строка 0 и столбец 0 содержат 0, потому что у пустого префикса нет ничего общего ни с чем. Порядок: каждая ячейка считывает значение ячейки сверху, слева и по диагонали сверху слева, поэтому при заполнении строка за строкой, слева направо, эти значения всегда уже готовы. Ответ — dp[n][m].
Для pear и reap строка для pea имеет вид [0, 0, 1, 2, 2]. Значение в её ячейке для rea равно 2, потому что буквы a совпадают: это значение в ячейке для pe и re, равное 1, плюс один. В последней ячейке сравниваются pear и reap: буквы r и p различаются, поэтому берётся большее из значений двух соседних ячеек — 2.
В таблице (n+1) × (m+1) ячеек, и для каждой требуется постоянное количество операций: около 10^6 шагов для двух строк длиной 1000 букв. Версия рекурсии с мемоизацией заполняет те же ячейки, но глубина рекурсии достигает n + m вызовов, что приводит к переполнению стека вызовов с размером по умолчанию в таких языках, как Python.
Алгоритм
- Создай таблицу
dpиз(n+1) × (m+1)нулей. - Для
iот 1 доnиjот 1 доmсравниtext1[i-1]сtext2[j-1]. - При совпадении присвой
dp[i][j] = dp[i-1][j-1] + 1. - Иначе присвой
dp[i][j] = max(dp[i-1][j], dp[i][j-1]). - Верни
dp[n][m].
def longestCommonSubsequence(text1, text2):
n, m = len(text1), len(text2)
# dp[i][j]: the longest common subsequence of text1[:i] and text2[:j].
# Row 0 and column 0 stay 0: an empty prefix has nothing in common.
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[n][m]Оставьте только две строки
Идея
Строка i таблицы считывает только строку i-1 и свои предыдущие ячейки. Когда строка готова, к строкам выше неё больше никогда не обращаются. Поэтому используй два массива: prev для готовой строки и cur для заполняемой строки, а после каждой строки меняй их местами. Рекуррентное соотношение и порядок остаются точно такими же.
Для общей подпоследовательности двух строк не имеет значения, какая из них первая, поэтому их можно поменять местами и обрабатывать строки вдоль более короткой. Тогда каждая строка содержит min(n, m) + 1 чисел: 1001 вместо миллиона ячеек для самых больших входных данных, при тех же 10^6 шагах работы.
Первый элемент каждой строки соответствует пустому префиксу более короткой строки, поэтому он должен оставаться равным 0. Ответ — последний элемент последней готовой строки.
Алгоритм
- Если
text2длиннее, чемtext1, поменяй их местами. - Создай
prevиcur, каждый из которых содержитm + 1нулей, гдеm— длина более короткой строки. - Для каждой буквы в
text1заполниcur[1..m]по тому же правилу, что и в таблице, используяprevдля предыдущей строки. - Поменяй
prevиcurместами. - Верни
prev[m].
def longestCommonSubsequence(text1, text2):
if len(text2) > len(text1):
text1, text2 = text2, text1 # keep the rows as short as the shorter string
m = len(text2)
# prev[j]: the answer for the previous prefix of text1 and text2[:j]
prev = [0] * (m + 1)
for ch in text1:
cur = [0] * (m + 1)
for j in range(1, m + 1):
if ch == text2[j - 1]:
cur[j] = prev[j - 1] + 1
else:
cur[j] = max(prev[j], cur[j - 1])
prev = cur
return prev[m]
Ловушки и крайние случаи
Рекуррентное соотношение короткое, и большинство ошибок — это ошибки на единицу или добавление совпадения не в том месте.
- Путаница между индексами таблицы и индексами строки. Ячейка
dp[i][j]сравниваетtext1[i-1]иtext2[j-1], потому что строка 0 соответствует пустому префиксу. - При совпадении прибавление единицы к
max(dp[i-1][j], dp[i][j-1])вместоdp[i-1][j-1]. Так одна буква может быть использована дважды: дляaaиaрезультатом будет 2 вместо 1. - Жадное сопоставление с помощью двух указателей. Для
cabиabcсопоставляются две буквы c, и результат равен 1, тогда как дляabон равен 2. - Запись в строку, из которой вы всё ещё читаете. При использовании двух строк каждое значение из строки выше должно браться из
prev, аcur[0]должно оставаться равным 0. - Решение задачи о наибольшей общей подстроке вместо этого. Подпоследовательность может пропускать буквы, а подстрока — нет.
- Мемоизация с рекурсией для строк длиной 1000 символов. Глубина вызовов достигает 2000, превышая стандартный предел Python, равный 1000.
Частые вопросы4
Какова временная сложность алгоритма поиска наибольшей общей подпоследовательности?
Решение с таблицей работает за время O(n × m), где n и m — длины двух последовательностей: оно заполняет одну ячейку для каждой пары префиксов. Для полной таблицы требуется O(n × m) памяти, а при использовании двух строк — O(min(n, m)). Простая рекурсия без таблицы имеет экспоненциальную сложность.
В чём разница между наибольшей общей подпоследовательностью и наибольшей общей подстрокой?
Подпоследовательность может пропускать буквы, если их порядок сохраняется, а подстрока представляет собой блок соседних букв. Для stone и longest наибольшая общая подпоследовательность — one (3), а наибольшая общая подстрока — on (2). Для варианта с подстрокой используется похожая таблица, но при несовпадении значение ячейки сбрасывается до 0, а не копируется из соседней.
Как вывести самую длинную общую подпоследовательность?
Заполни таблицу целиком, затем пройди обратно от dp[n][m]. Если две буквы в текущей ячейке совпадают, эта буква входит в ответ: запиши её и перейди по диагонали вверх и влево. Иначе перейди к соседней ячейке сверху или слева, в которой хранится большее значение. В конце переверни записанные буквы. Версия с двумя строками не позволяет это сделать, потому что предыдущие строки уже отброшены.
Как LCS связан с инструментами diff и расстоянием редактирования?
Сравнение двух версий файла с помощью diff находит их наибольшую общую подпоследовательность строк; каждая строка вне неё отображается как добавленная или удалённая. Аналогично, минимальное количество вставок и удалений, необходимых для преобразования одной строки в другую, равно n + m - 2 × LCS. Расстояние редактирования также допускает замену буквы, поэтому для него используется собственная таблица с третьим вариантом для каждой ячейки.
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def longestCommonSubsequence(text1, text2):
# Напишите код здесьСлучай 1
Случай 2
Случай 3
Ввод
text1 = "stone" text2 = "longest"
Ожидается
3