ДП на префиксах двух строк: как реализовать редакционное расстояние (Левенштейна) за O(nm)?
Нужно расстояние Левенштейна между строками a и b: минимум операций вставки/удаления/замены символа, чтобы превратить одну в другую. Понимаю, что это ДП на префиксах, но путаюсь в граничных условиях и в том, какие три перехода брать.
2 ответа
dp[i][j] = минимальное число операций, чтобы превратить первые i символов a в первые j символов b (ДП на префиксах). Переходы по последнему символу:
- удалить
a[i-1]:dp[i-1][j] + 1 - вставить
b[j-1]:dp[i][j-1] + 1 - заменить/совпадение:
dp[i-1][j-1] + (a[i-1] != b[j-1] ? 1 : 0)
int n = a.size(), m = b.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
for (int i = 0; i <= n; ++i) dp[i][0] = i; // удалить i символов
for (int j = 0; j <= m; ++j) dp[0][j] = j; // вставить j символов
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) {
int cost = (a[i - 1] == b[j - 1]) ? 0 : 1;
dp[i][j] = min({ dp[i - 1][j] + 1, // удаление
dp[i][j - 1] + 1, // вставка
dp[i - 1][j - 1] + cost // замена/совпадение
});
}
// ответ dp[n][m]
Граничные условия — ключ к корректности: dp[i][0]=i (превратить префикс в пустую строку = i удалений), dp[0][j]=j (из пустой строки = j вставок).
Время $O(nm)$, память $O(nm)$ (ужимается до $O(\min(n,m))$ двумя слоями, если восстановление не нужно).
Тот же каркас «ДП на префиксах двух строк» переиспользуется для родственных задач — меняются только переходы:
- LCS (наибольшая общая подпоследовательность): если
a[i-1]==b[j-1]→dp[i-1][j-1]+1, иначеmax(dp[i-1][j], dp[i][j-1]). - проверка вхождения как подпоследовательности, edit distance с разной ценой операций, выравнивание последовательностей (биоинформатика).
Частая ошибка — перепутать, что значит dp[i][j] (префиксы длины i,j, а индексы строк i-1,j-1 из-за 1-индексации таблицы). И при ужатии до 2 слоёв легко затереть dp[i-1][j-1] (диагональ) — её надо сохранить во временную переменную до перезаписи.