mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-09 05:56:06 +00:00
First version.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
# Задача о редакционном расстоянии
|
||||
|
||||
Редакционное расстояние, также называемое расстоянием Левенштейна, указывает минимальное количество изменений, необходимых для взаимного преобразования двух строк, и обычно используется в информационном поиске и обработке естественного языка для измерения сходства двух последовательностей.
|
||||
|
||||
!!! question
|
||||
|
||||
Даны две строки $s$ и $t$, верните минимальное количество шагов редактирования, необходимых для преобразования $s$ в $t$.
|
||||
|
||||
Вы можете выполнять три операции редактирования в строке: вставить символ, удалить символ, заменить символ на любой другой символ.
|
||||
|
||||
Как показано на рисунке ниже, для преобразования `kitten` в `sitting` требуется 3 шага редактирования, включая 2 операции замены и 1 операцию добавления; для преобразования `hello` в `algo` требуется 3 шага, включая 2 операции замены и 1 операцию удаления.
|
||||
|
||||

|
||||
|
||||
**Задачу о редакционном расстоянии можно естественным образом объяснить с помощью модели дерева решений**. Строки соответствуют узлам дерева, один раунд решения (одна операция редактирования) соответствует ребру дерева.
|
||||
|
||||
Как показано на рисунке ниже, без ограничения операций каждый узел может породить множество ребер, каждое ребро соответствует одной операции, что означает, что существует множество возможных путей преобразования от `hello` к `algo`.
|
||||
|
||||
С точки зрения дерева решений цель этой задачи -- найти кратчайший путь между узлом `hello` и узлом `algo`.
|
||||
|
||||

|
||||
|
||||
### Подход динамического программирования
|
||||
|
||||
**Первый шаг: обдумать решение на каждом раунде, определить состояние, чтобы получить таблицу $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$. Следовательно, состояние -- это текущие рассматриваемые $i$-й и $j$-й символы в $s$ и $t$, обозначаемые как $[i, j]$.
|
||||
|
||||
Состояние $[i, j]$ соответствует подзадаче: **минимальное количество шагов редактирования, необходимых для изменения первых $i$ символов $s$ на первые $j$ символов $t$**.
|
||||
|
||||
Таким образом, получаем двумерную таблицу $dp$ размером $(i+1) \times (j+1)$.
|
||||
|
||||
**Второй шаг: найти оптимальную подструктуру, затем вывести уравнение перехода состояния**
|
||||
|
||||
Рассмотрим подзадачу $dp[i, j]$, символы в конце соответствующих двух строк -- это $s[i-1]$ и $t[j-1]$, можно разделить на три случая, показанные на рисунке ниже, в зависимости от различных операций редактирования.
|
||||
|
||||
1. Добавить $t[j-1]$ после $s[i-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]
|
||||
$$
|
||||
|
||||
**Третий шаг: определить граничные условия и порядок перехода состояния**
|
||||
|
||||
Когда обе строки пусты, количество шагов редактирования равно $0$, то есть $dp[0, 0] = 0$. Когда $s$ пуста, но $t$ не пуста, минимальное количество шагов редактирования равно длине $t$, то есть первая строка $dp[0, j] = j$. Когда $s$ не пуста, но $t$ пуста, минимальное количество шагов редактирования равно длине $s$, то есть первый столбец $dp[i, 0] = i$.
|
||||
|
||||
Наблюдая за уравнением перехода состояния, решение $dp[i, j]$ зависит от решений слева, сверху и слева-сверху, поэтому можно обойти всю таблицу $dp$ в прямом порядке с помощью двух циклов.
|
||||
|
||||
### Реализация кода
|
||||
|
||||
```src
|
||||
[file]{edit_distance}-[class]{}-[func]{edit_distance_dp}
|
||||
```
|
||||
|
||||
Как показано на рисунке ниже, процесс перехода состояния задачи о редакционном расстоянии очень похож на задачу о рюкзаке, оба можно рассматривать как процесс заполнения двумерной сетки.
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
Reference in New Issue
Block a user