Редакция для Восстановление общей подпоследовательности
Submitting an official solution before solving the problem yourself is a bannable offence.
1. Идея
Нужно найти любую наибольшую общую подпоследовательность двух строк s и t.
Подпоследовательность — это символы, выбранные в том же порядке, но не обязательно подряд. Значит, задача не про отрезки строки, а именно про выбор некоторых позиций.
Классический подход здесь — динамическое программирование.
Будем хранить в таблице dp[i][j] длину наибольшей общей подпоследовательности суффиксов:
- строки
s, начиная с позицииi, - строки
t, начиная с позицииj.
После того как длины посчитаны, можно восстановить один из оптимальных ответов, двигаясь по таблице.
2. Наблюдения
Наблюдение 1
Если s[i] == t[j], то этот символ можно взять в общую подпоследовательность.
Тогда:
- первый символ ответа на этих суффиксах — это
s[i], - дальше остаётся решить ту же задачу для позиций
i + 1иj + 1.
Значит:
dp[i][j] = 1 + dp[i + 1][j + 1]
Наблюдение 2
Если s[i] != t[j], то одновременно взять оба этих символа нельзя.
Тогда один из них нужно пропустить:
- либо пропустить
s[i]и перейти кdp[i + 1][j], - либо пропустить
t[j]и перейти кdp[i][j + 1].
Берём лучший вариант:
dp[i][j] = max(dp[i + 1][j], dp[i][j + 1])
Наблюдение 3
Если хотя бы одна строка закончилась, то общей подпоследовательности больше быть не может.
Поэтому:
dp[n][j] = 0для всехj,dp[i][m] = 0для всехi.
Здесь n = len(s), m = len(t).
Наблюдение 4
Одной длины недостаточно: по условию нужно вывести саму подпоследовательность. Но если таблица dp уже построена, восстановление делается жадным проходом:
- если
s[i] == t[j], добавляем этот символ в ответ; - иначе идём туда, где значение
dpне меньше.
Так мы не потеряем оптимальную длину.
3. Алгоритм
Пусть n = |s|, m = |t|.
Шаг 1. Строим таблицу dp
Создаём массив dp размера (n + 1) x (m + 1) и заполняем его нулями.
Далее идём с конца строк к началу:
- для
iотn - 1до0, - для
jотm - 1до0.
Переходы:
- если
s[i] == t[j], тоdp[i][j] = 1 + dp[i + 1][j + 1]; - иначе
dp[i][j] = max(dp[i + 1][j], dp[i][j + 1]).
Шаг 2. Восстанавливаем ответ
Запускаем два указателя:
i = 0,j = 0.
Пока i < n и j < m:
- если
s[i] == t[j], добавляемs[i]в ответ и увеличиваем оба указателя; - иначе сравниваем
dp[i + 1][j]иdp[i][j + 1]:- если
dp[i + 1][j] >= dp[i][j + 1], увеличиваемi; - иначе увеличиваем
j.
- если
Полученная строка и будет одной из наибольших общих подпоследовательностей.
4. Почему это работает
Докажем корректность идеи по частям.
Почему правильно считается dp
Рассмотрим значение dp[i][j], то есть ответ для суффиксов s[i:] и t[j:].
Случай 1: s[i] == t[j]
Тогда можно взять этот символ в общую подпоследовательность. После этого остаётся найти оптимальный ответ для хвостов s[i+1:] и t[j+1:].
Значит, длина равна:
1 + dp[i + 1][j + 1]
Это корректно, потому что выбранный символ одинаков в обеих строках и стоит в начале рассматриваемых суффиксов.
Случай 2: s[i] != t[j]
Тогда нельзя одновременно использовать оба текущих символа как следующий символ общей подпоследовательности.
Значит, в любом оптимальном ответе обязательно:
- либо не используется
s[i], - либо не используется
t[j].
То есть оптимальный ответ совпадает с лучшим из двух вариантов:
dp[i + 1][j],dp[i][j + 1].
Следовательно:
dp[i][j] = max(dp[i + 1][j], dp[i][j + 1])
Так мы перебираем все необходимые возможности и не упускаем оптимум.
Почему восстановление даёт правильный ответ
Во время восстановления мы всегда находимся в клетке dp[i][j], которая хранит длину оптимального ответа для текущих суффиксов.
- Если
s[i] == t[j], то этот символ может быть первым символом оптимальной подпоследовательности, и переход вdp[i + 1][j + 1]сохраняет оптимальность. - Если символы не равны, то:
- если
dp[i + 1][j] >= dp[i][j + 1], можно безопасно пропуститьs[i]; - иначе безопасно пропустить
t[j].
- если
В обоих случаях мы переходим в состояние, где длина оптимального ответа не уменьшается по сравнению с тем, что ещё нужно набрать.
Значит, собранная строка имеет длину dp[0][0] и является наибольшей общей подпоследовательностью.
5. Сложность
Таблица dp имеет (n + 1) * (m + 1) клеток, и каждая заполняется за O(1).
Поэтому:
- время:
O(n * m) - память:
O(n * m)
При ограничениях |s|, |t| <= 1000 это подходит.
6. Код на C++17
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
int main() {
string s, t;
cin >> s >> t;
int n = (int)s.size();
int m = (int)t.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
for (int i = n - 1; i >= 0; --i) {
for (int j = m - 1; j >= 0; --j) {
if (s[i] == t[j]) {
dp[i][j] = 1 + dp[i + 1][j + 1];
} else {
dp[i][j] = max(dp[i + 1][j], dp[i][j + 1]);
}
}
}
string ans;
int i = 0, j = 0;
while (i < n && j < m) {
if (s[i] == t[j]) {
ans += s[i];
++i;
++j;
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
++i;
} else {
++j;
}
}
cout << ans << "\n";
return 0;
}
7. Код на Python 3
s = input().strip()
t = input().strip()
n = len(s)
m = len(t)
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n - 1, -1, -1):
for j in range(m - 1, -1, -1):
if s[i] == t[j]:
dp[i][j] = 1 + dp[i + 1][j + 1]
else:
if dp[i + 1][j] >= dp[i][j + 1]:
dp[i][j] = dp[i + 1][j]
else:
dp[i][j] = dp[i][j + 1]
ans = []
i = 0
j = 0
while i < n and j < m:
if s[i] == t[j]:
ans.append(s[i])
i += 1
j += 1
elif dp[i + 1][j] >= dp[i][j + 1]:
i += 1
else:
j += 1
print("".join(ans))
Комментарии