Перейти к содержанию

Задача о расстоянии редактирования

Расстояние редактирования, также называемое расстоянием Левенштейна, - это минимальное количество изменений, необходимых для преобразования одной строки в другую. Обычно оно используется для измерения сходства двух последовательностей в информационном поиске и обработке естественного языка.

Question

Даны две строки \(s\) и \(t\) . Верните минимальное число шагов редактирования, необходимое для преобразования \(s\) в \(t\) .

Для строки допускаются три операции редактирования: вставка одного символа, удаление одного символа и замена одного символа на произвольный другой символ.

Как показано на рисунке ниже, для преобразования kitten в sitting требуется 3 шага редактирования: 2 операции замены и 1 операция вставки. Для преобразования hello в algo также требуется 3 шага: 2 замены и 1 удаление.

Пример данных для задачи о расстоянии редактирования

Задачу о расстоянии редактирования можно естественным образом объяснить с помощью модели дерева решений. Строки соответствуют узлам дерева, а один шаг решения, то есть одна операция редактирования, соответствует одному ребру дерева.

Как показано на рисунке ниже, если не ограничивать число операций, то каждый узел может порождать множество ребер, и каждое из них соответствует одному из вариантов преобразования. Это означает, что преобразовать hello в algo можно множеством разных путей.

С точки зрения дерева решений цель этой задачи - найти кратчайший путь между узлом hello и узлом algo .

Представление задачи о расстоянии редактирования через дерево решений

Идея динамического программирования

Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу \(dp\)

На каждом раунде решение состоит в выполнении одной операции редактирования над строкой \(s\) .

Нам нужно, чтобы в ходе выполнения операций размер задачи постепенно уменьшался. Только тогда можно строить подзадачи. Пусть длины строк \(s\) и \(t\) равны соответственно \(n\) и \(m\). Сначала рассмотрим последние символы этих строк, то есть \(s[n-1]\) и \(t[m-1]\) .

  • Если \(s[n-1]\) и \(t[m-1]\) совпадают, их можно просто пропустить и сразу перейти к сравнению \(s[n-2]\) и \(t[m-2]\) .
  • Если \(s[n-1]\) и \(t[m-1]\) различны, нужно выполнить над \(s\) одну операцию редактирования (вставку, удаление или замену), чтобы последние символы стали одинаковыми, после чего можно перейти к задаче меньшего размера.

Иначе говоря, каждый шаг решения, то есть операция редактирования над строкой \(s\) , меняет те символы, которые еще необходимо сопоставить в строках \(s\) и \(t\) . Поэтому состояние определяется текущими позициями рассматриваемых символов в \(s\) и \(t\) , то есть состоянием \([i, j]\) .

Подзадача, соответствующая состоянию \([i, j]\) , такова: минимальное число операций редактирования, необходимое для преобразования первых \(i\) символов строки \(s\) в первые \(j\) символов строки \(t\).

Отсюда получается двумерная таблица \(dp\) размера \((i+1) \times (j+1)\) .

Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния

Рассмотрим подзадачу \(dp[i, j]\) . Ее последние символы - это \(s[i-1]\) и \(t[j-1]\) . В зависимости от операции редактирования возможны три случая, показанные на рисунке ниже.

  1. Вставить после \(s[i-1]\) символ \(t[j-1]\). Тогда остается подзадача \(dp[i, j-1]\) .
  2. Удалить \(s[i-1]\). Тогда остается подзадача \(dp[i-1, j]\) .
  3. Заменить \(s[i-1]\) на \(t[j-1]\). Тогда остается подзадача \(dp[i-1, j-1]\) .

Переходы состояния в задаче о расстоянии редактирования

Согласно этому анализу оптимальная подструктура такова: минимальное число шагов редактирования для \(dp[i, j]\) равно минимуму из трех значений - \(dp[i, j-1]\) , \(dp[i-1, j]\) и \(dp[i-1, j-1]\) - плюс \(1\) шаг за текущее редактирование. Значит, уравнение перехода состояния имеет вид:

\[ dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \]

Заметим, что если символы \(s[i-1]\) и \(t[j-1]\) совпадают, то редактировать текущий символ не нужно. В этом случае уравнение перехода состояния имеет вид:

\[ dp[i, j] = dp[i-1, j-1] \]

Шаг 3: определить граничные условия и порядок переходов

Когда обе строки пусты, число операций редактирования равно \(0\) , то есть \(dp[0, 0] = 0\) . Когда строка \(s\) пуста, а строка \(t\) непуста, минимальное число операций равно длине строки \(t\) , то есть вся первая строка инициализируется как \(dp[0, j] = j\) . Когда строка \(s\) непуста, а строка \(t\) пуста, минимальное число операций равно длине строки \(s\) , то есть весь первый столбец инициализируется как \(dp[i, 0] = i\) .

Из уравнения перехода видно, что решение \(dp[i, j]\) зависит от значений слева, сверху и слева сверху, поэтому всю таблицу \(dp\) можно обходить двумя вложенными циклами в прямом порядке.

Реализация кода

[file]{edit_distance}-[class]{}-[func]{edit_distance_dp}

Как показано на рисунке ниже, процесс переходов состояния в задаче о расстоянии редактирования очень похож на задачи о рюкзаке: и там и здесь его можно рассматривать как заполнение двумерной сетки.

Процесс динамического программирования для расстояния редактирования

edit_distance_dp_step2

edit_distance_dp_step3

edit_distance_dp_step4

edit_distance_dp_step5

edit_distance_dp_step6

edit_distance_dp_step7

edit_distance_dp_step8

edit_distance_dp_step9

edit_distance_dp_step10

edit_distance_dp_step11

edit_distance_dp_step12

edit_distance_dp_step13

edit_distance_dp_step14

edit_distance_dp_step15

Оптимизация пространства

Поскольку \(dp[i,j]\) зависит от значения сверху \(dp[i-1, j]\) , слева \(dp[i, j-1]\) и слева сверху \(dp[i-1, j-1]\) , прямой обход после оптимизации памяти теряет значение слева сверху, а обратный обход не позволяет заранее построить значение слева \(dp[i, j-1]\) . Значит, оба наивных варианта обхода здесь непригодны.

Чтобы решить эту проблему, можно использовать переменную leftup для временного сохранения значения слева сверху \(dp[i-1, j-1]\). После этого остается учитывать только верхнее и левое значения. Тогда ситуация становится аналогичной задаче о полном рюкзаке, и можно выполнять прямой обход. Код приведен ниже:

[file]{edit_distance}-[class]{}-[func]{edit_distance_dp_comp}