1 added
1 removed
Original
2026-01-01
Modified
2026-02-26
1
<p>Люди часто ошибаются при вводе текста. Именно поэтому компьютеры научились понимать слова с опечатками:</p>
1
<p>Люди часто ошибаются при вводе текста. Именно поэтому компьютеры научились понимать слова с опечатками:</p>
2
<p>Google понимает, что вместо слова ТОКОЕ имеется в виду слово ТАКОЕ. Но как он это делает? Вряд ли можно составить словарь всех опечаток. Один из способов найти подходящее слово заключается в расчете редакционного расстояния.</p>
2
<p>Google понимает, что вместо слова ТОКОЕ имеется в виду слово ТАКОЕ. Но как он это делает? Вряд ли можно составить словарь всех опечаток. Один из способов найти подходящее слово заключается в расчете редакционного расстояния.</p>
3
<p><strong>Редакционное расстояние</strong>- это минимальное количество букв, которые нужно вставить, удалить или заменить, чтобы получить из одного слова другое.</p>
3
<p><strong>Редакционное расстояние</strong>- это минимальное количество букв, которые нужно вставить, удалить или заменить, чтобы получить из одного слова другое.</p>
4
<p>Рассмотрим пару примеров. Чтобы превратить:</p>
4
<p>Рассмотрим пару примеров. Чтобы превратить:</p>
5
<ul><li>СТОЛ в СТОП, надо заменить букву Л на П</li>
5
<ul><li>СТОЛ в СТОП, надо заменить букву Л на П</li>
6
<li>СТОЛ в СТОЛЫ - добавить букву Ы</li>
6
<li>СТОЛ в СТОЛЫ - добавить букву Ы</li>
7
<li>СТОЛ в СТО - удалить букву Л</li>
7
<li>СТОЛ в СТО - удалить букву Л</li>
8
</ul><p>В каждом из этих случаев редакционное расстояние равно единице.</p>
8
</ul><p>В каждом из этих случаев редакционное расстояние равно единице.</p>
9
<p>Чтобы превратить СТОЛ в ТОНА, потребуется три преобразования:</p>
9
<p>Чтобы превратить СТОЛ в ТОНА, потребуется три преобразования:</p>
10
<ul><li>СТОЛ → ТОЛ</li>
10
<ul><li>СТОЛ → ТОЛ</li>
11
<li>ТОЛ → ТОН</li>
11
<li>ТОЛ → ТОН</li>
12
<li>ТОН → ТОНА</li>
12
<li>ТОН → ТОНА</li>
13
</ul><p>Следовательно, редакционное расстояние между словами СТОЛ и ТОНА равно трем.</p>
13
</ul><p>Следовательно, редакционное расстояние между словами СТОЛ и ТОНА равно трем.</p>
14
<p>Если мы не обнаружили слово в словаре, мы можем поискать слова, которые находятся от него на небольшом редакционном расстоянии. Слова ТОКОЕ в словаре нет, но есть слово ТАКОЕ. Редакционное расстояние между словами равно единице, поэтому ТАКОЕ - хороший кандидат на замену.</p>
14
<p>Если мы не обнаружили слово в словаре, мы можем поискать слова, которые находятся от него на небольшом редакционном расстоянии. Слова ТОКОЕ в словаре нет, но есть слово ТАКОЕ. Редакционное расстояние между словами равно единице, поэтому ТАКОЕ - хороший кандидат на замену.</p>
15
<p>Но как вычислять редакционное расстояние?</p>
15
<p>Но как вычислять редакционное расстояние?</p>
16
<h2>Решаем задачу "в лоб"</h2>
16
<h2>Решаем задачу "в лоб"</h2>
17
<p>В решении этой задачи нам помогут графы. Для начала попробуем сформулировать алгоритм расчета редакционного расстояния, но это не так просто сделать.</p>
17
<p>В решении этой задачи нам помогут графы. Для начала попробуем сформулировать алгоритм расчета редакционного расстояния, но это не так просто сделать.</p>
18
<p>Когда мы сталкиваемся с опечаткой, мы определяем, что именно пошло не так:</p>
18
<p>Когда мы сталкиваемся с опечаткой, мы определяем, что именно пошло не так:</p>
19
<ul><li>Вместо правильной буквы стоит неправильная</li>
19
<ul><li>Вместо правильной буквы стоит неправильная</li>
20
<li>Нужная буква пропущена</li>
20
<li>Нужная буква пропущена</li>
21
<li>В слове появилась лишняя буква</li>
21
<li>В слове появилась лишняя буква</li>
22
</ul><p>Представим, что мы встретили слово ТОКОЕ и поняли, что здесь есть опечатка - нужно поставить А вместо О. Алгоритм мог бы по буквам сравнить слова ТОКОЕ и ТАКОЕ и обнаружить расхождение во второй букве. Но вдруг речь идет не о замене, а о лишней букве? Чтобы принять решение, надо заглянуть на одну букву вперед. Если речь идет о двух или трех буквах, заглядывать придется еще дальше.</p>
22
</ul><p>Представим, что мы встретили слово ТОКОЕ и поняли, что здесь есть опечатка - нужно поставить А вместо О. Алгоритм мог бы по буквам сравнить слова ТОКОЕ и ТАКОЕ и обнаружить расхождение во второй букве. Но вдруг речь идет не о замене, а о лишней букве? Чтобы принять решение, надо заглянуть на одну букву вперед. Если речь идет о двух или трех буквах, заглядывать придется еще дальше.</p>
23
<p>Кажется, такой код будет состоять из бесчисленных операторов if, вложенных друг в друга. Это не самый простой способ.</p>
23
<p>Кажется, такой код будет состоять из бесчисленных операторов if, вложенных друг в друга. Это не самый простой способ.</p>
24
<p>Решение через графы заключается в том, что мы допускаем все возможные варианты и выбираем среди них вариант с наименьшим расстоянием. Предположим, мы хотим узнать редакционное расстояние для пары слов ВХОД → ВДОХ.</p>
24
<p>Решение через графы заключается в том, что мы допускаем все возможные варианты и выбираем среди них вариант с наименьшим расстоянием. Предположим, мы хотим узнать редакционное расстояние для пары слов ВХОД → ВДОХ.</p>
25
<p>Первая буква - В - совпадает в обоих случаях. При этом мы не можем исключить и вариантов с лишней или пропущенной буквой. Чтобы убедиться в этом, изучим такие примеры</p>
25
<p>Первая буква - В - совпадает в обоих случаях. При этом мы не можем исключить и вариантов с лишней или пропущенной буквой. Чтобы убедиться в этом, изучим такие примеры</p>
26
<ul><li>Пара ВВЕСТИ → ВЕСТИ. Чтобы превратить первое слово во второе, нужно убрать первую букву В - то есть она лишняя</li>
26
<ul><li>Пара ВВЕСТИ → ВЕСТИ. Чтобы превратить первое слово во второе, нужно убрать первую букву В - то есть она лишняя</li>
27
<li>Пара ВЕСТИ → ВВЕСТИ. Чтобы превратить первое слово во второе, придется вставить букву В - она недостающая</li>
27
<li>Пара ВЕСТИ → ВВЕСТИ. Чтобы превратить первое слово во второе, придется вставить букву В - она недостающая</li>
28
</ul><p>Обратите внимание на интересную симметрию. Вставка буквы в начало первого слова - по сути то же самое, что и удаление первой буквы из второго слова.</p>
28
</ul><p>Обратите внимание на интересную симметрию. Вставка буквы в начало первого слова - по сути то же самое, что и удаление первой буквы из второго слова.</p>
29
<p>Начинаем сравнивать слова слева направо. На рисунке показана вершина графа решения:</p>
29
<p>Начинаем сравнивать слова слева направо. На рисунке показана вершина графа решения:</p>
30
<p>Рассмотрим граф подробнее:</p>
30
<p>Рассмотрим граф подробнее:</p>
31
<ul><li>Левый дочерний узел соответствует удалению (У). Удаление увеличивает редакционное расстояние на единицу. Если мы уберем первую букву слово ВХОД превратится в ХОД, а слово ВДОХ не изменится</li>
31
<ul><li>Левый дочерний узел соответствует удалению (У). Удаление увеличивает редакционное расстояние на единицу. Если мы уберем первую букву слово ВХОД превратится в ХОД, а слово ВДОХ не изменится</li>
32
<li>Средний узел - это замена (З). Нам не нужно менять В на В, поэтому редакционное расстояние не увеличивается. После сравнения отбрасываем первые буквы в обоих словах, так что у нас остаются ХОД и ДОХ</li>
32
<li>Средний узел - это замена (З). Нам не нужно менять В на В, поэтому редакционное расстояние не увеличивается. После сравнения отбрасываем первые буквы в обоих словах, так что у нас остаются ХОД и ДОХ</li>
33
<li>Правый дочерний узел - это вставка (В). Вставка буквы в первое слово - то же самое, что удаление буквы из второго слова. В итоге получаем пару ВХОД и ДОХ, при этом редакционное расстояние увеличивается на единицу</li>
33
<li>Правый дочерний узел - это вставка (В). Вставка буквы в первое слово - то же самое, что удаление буквы из второго слова. В итоге получаем пару ВХОД и ДОХ, при этом редакционное расстояние увеличивается на единицу</li>
34
</ul><p>В общем, мы не просто вычисляем редакционного расстояния между словами ВХОД и ВДОХ. Мы сводим этот процесс к вычислению расстояний для трех более коротких слов:</p>
34
</ul><p>В общем, мы не просто вычисляем редакционного расстояния между словами ВХОД и ВДОХ. Мы сводим этот процесс к вычислению расстояний для трех более коротких слов:</p>
35
<ul><li>ХОД и ВДОХ</li>
35
<ul><li>ХОД и ВДОХ</li>
36
<li>ХОД и ДОХ</li>
36
<li>ХОД и ДОХ</li>
37
<li>ВХОД и ДОХ</li>
37
<li>ВХОД и ДОХ</li>
38
</ul><p>Выше мы свели эту сложную задачу к простой - значит, в этом случае будет удобно использовать рекурсию.</p>
38
</ul><p>Выше мы свели эту сложную задачу к простой - значит, в этом случае будет удобно использовать рекурсию.</p>
39
<p>В курсе по алгоритмам мы знакомились с рекурсивными алгоритмами. Рекурсия завершится, когда в обоих словах не останется букв. Тогда эти пустые слова совпадут друг с другом, поэтому редакционное расстояние между ними будет равняться нулю.</p>
39
<p>В курсе по алгоритмам мы знакомились с рекурсивными алгоритмами. Рекурсия завершится, когда в обоих словах не останется букв. Тогда эти пустые слова совпадут друг с другом, поэтому редакционное расстояние между ними будет равняться нулю.</p>
40
<p>На рисунке показана часть графа, которая соответствует решению задачи:</p>
40
<p>На рисунке показана часть графа, которая соответствует решению задачи:</p>
41
<p>Если буквы остались только в первом слове, то мы сравниваем это слово с пустой строкой. Нам доступны только операции удаления:</p>
41
<p>Если буквы остались только в первом слове, то мы сравниваем это слово с пустой строкой. Нам доступны только операции удаления:</p>
42
<ul><li>ХОД → ОД</li>
42
<ul><li>ХОД → ОД</li>
43
<li>ОД → Д</li>
43
<li>ОД → Д</li>
44
<li>Д → пустая строка</li>
44
<li>Д → пустая строка</li>
45
</ul><p>Аналогично, если буквы остались только во втором слове, нам доступны только операции вставки:</p>
45
</ul><p>Аналогично, если буквы остались только во втором слове, нам доступны только операции вставки:</p>
46
<ul><li>Пустая строка → Д</li>
46
<ul><li>Пустая строка → Д</li>
47
<li>Д → ОД</li>
47
<li>Д → ОД</li>
48
<li>ОД → ХОД</li>
48
<li>ОД → ХОД</li>
49
-
</ul><p>Замена возможна, только если буквы для сравнения остались в обоих словах.</p>
49
+
</ul><p>Замена возможна, только если буквы для сравнения остались в обоих ��ловах.</p>
50
<p>Граф решения может включать самые экзотические варианты преобразований. Например:</p>
50
<p>Граф решения может включать самые экзотические варианты преобразований. Например:</p>
51
<ul><li>ВХОД → ХОД (удаление)</li>
51
<ul><li>ВХОД → ХОД (удаление)</li>
52
<li>ХОД → ОД (удаление)</li>
52
<li>ХОД → ОД (удаление)</li>
53
<li>ОД → Д (удаление)</li>
53
<li>ОД → Д (удаление)</li>
54
<li>Д → Х (замена)</li>
54
<li>Д → Х (замена)</li>
55
<li>Х → ОХ (вставка)</li>
55
<li>Х → ОХ (вставка)</li>
56
<li>ОХ → ДОХ (вставка)</li>
56
<li>ОХ → ДОХ (вставка)</li>
57
<li>ДОХ → ВДОХ (вставка)</li>
57
<li>ДОХ → ВДОХ (вставка)</li>
58
</ul><p>Это тоже решение задачи. Но редакционное расстояние определяется как минимальное количество операций. Поэтому на каждом шаге мы вычисляем три дочерних расстояния и мы выбираем наименьшее из них.</p>
58
</ul><p>Это тоже решение задачи. Но редакционное расстояние определяется как минимальное количество операций. Поэтому на каждом шаге мы вычисляем три дочерних расстояния и мы выбираем наименьшее из них.</p>
59
<p>Наименьшее редакционное расстояние между словами ВХОД → ВДОХ равно двум. Оно соответствует двум заменам:</p>
59
<p>Наименьшее редакционное расстояние между словами ВХОД → ВДОХ равно двум. Оно соответствует двум заменам:</p>
60
<ul><li>ВХОД → ВДОД</li>
60
<ul><li>ВХОД → ВДОД</li>
61
<li>ВДОД → ВДОХ</li>
61
<li>ВДОД → ВДОХ</li>
62
</ul><h2>Реализуем первый алгоритм</h2>
62
</ul><h2>Реализуем первый алгоритм</h2>
63
<p>Реализация алгоритма не очень сложная. От описанного алгоритма она отличается небольшой оптимизацией.</p>
63
<p>Реализация алгоритма не очень сложная. От описанного алгоритма она отличается небольшой оптимизацией.</p>
64
<p>Дело в том, что удаление буквы из начала слова - ресурсоемкая операция. Вместо удаления мы заводим переменные index1 и index2, которые указывают на очередную букву в первом и во втором слове. Другими словами, они хранят порядковый номер буквы, начиная с нуля.</p>
64
<p>Дело в том, что удаление буквы из начала слова - ресурсоемкая операция. Вместо удаления мы заводим переменные index1 и index2, которые указывают на очередную букву в первом и во втором слове. Другими словами, они хранят порядковый номер буквы, начиная с нуля.</p>
65
<p>Вместо реального удаления буквы мы просто увеличиваем index1 или index2 на единицу:</p>
65
<p>Вместо реального удаления буквы мы просто увеличиваем index1 или index2 на единицу:</p>
66
<p>Внутри функции distance() мы создаем рекурсивную функцию recursive(). В самом начале этой функции мы проверяем, есть ли у нас буквы для сравнения хотя бы в одном слове.</p>
66
<p>Внутри функции distance() мы создаем рекурсивную функцию recursive(). В самом начале этой функции мы проверяем, есть ли у нас буквы для сравнения хотя бы в одном слове.</p>
67
<p>Если букв нет, мы дошли до конечного узла, в нем сравниваются два пустых слова. Значит, результат равен нулю:</p>
67
<p>Если букв нет, мы дошли до конечного узла, в нем сравниваются два пустых слова. Значит, результат равен нулю:</p>
68
<p>Иногда у нас будет три дочерних расстояния, а иногда - только одно. Чтобы обрабатывать эти варианты универсальным способом, создадим массив дочерних расстояний:</p>
68
<p>Иногда у нас будет три дочерних расстояния, а иногда - только одно. Чтобы обрабатывать эти варианты универсальным способом, создадим массив дочерних расстояний:</p>
69
<p>Если буквы в словах совпадают, замена не нужна - ее стоимость равна нулю. Если буквы различаются, добавляем единицу. В случае замены мы избавляемся от обеих букв в начале слова. Поэтому при рекурсивном вызове увеличиваем на единицу обе переменные - index1 и index2:</p>
69
<p>Если буквы в словах совпадают, замена не нужна - ее стоимость равна нулю. Если буквы различаются, добавляем единицу. В случае замены мы избавляемся от обеих букв в начале слова. Поэтому при рекурсивном вызове увеличиваем на единицу обе переменные - index1 и index2:</p>
70
<p>Удаление и вставка очень похожи друг на друга. Вставка буквы в первое слово - то же самое, что и удаление буквы из второго слова:</p>
70
<p>Удаление и вставка очень похожи друг на друга. Вставка буквы в первое слово - то же самое, что и удаление буквы из второго слова:</p>
71
<p>Завершаем рекурсивную функцию возвратом минимального расстояния. Функция distance() вызывает ее с параметрами 0 и 0, которые соответствуют первым буквам:</p>
71
<p>Завершаем рекурсивную функцию возвратом минимального расстояния. Функция distance() вызывает ее с параметрами 0 и 0, которые соответствуют первым буквам:</p>
72
<p>Проверим работу функции:</p>
72
<p>Проверим работу функции:</p>
73
<p>Алгоритм кажется хорошим, пока мы не оценили его сложность. На каждом шаге мы строим три дочерних узла, перемещаясь при этом на одну букву. Количество узлов в таком графе равно 3N+M, где N и M - длины первого и второго слова. Это верхняя граница. Реальное количество узлов будет меньше, но экспоненциальный рост количества узлов налицо.</p>
73
<p>Алгоритм кажется хорошим, пока мы не оценили его сложность. На каждом шаге мы строим три дочерних узла, перемещаясь при этом на одну букву. Количество узлов в таком графе равно 3N+M, где N и M - длины первого и второго слова. Это верхняя граница. Реальное количество узлов будет меньше, но экспоненциальный рост количества узлов налицо.</p>
74
<p>Для вычисления редакционного расстояния между словами ВХОД и ВДОХ придется построить 520 узлов, что довольно много. Нет ли способа ускорить этот алгоритм?</p>
74
<p>Для вычисления редакционного расстояния между словами ВХОД и ВДОХ придется построить 520 узлов, что довольно много. Нет ли способа ускорить этот алгоритм?</p>
75
<h2>Ускоряем алгоритм</h2>
75
<h2>Ускоряем алгоритм</h2>
76
<p>Посмотрим на граф решения и обнаружим одни и те же узлы в разных его местах:</p>
76
<p>Посмотрим на граф решения и обнаружим одни и те же узлы в разных его местах:</p>
77
<p>На рисунке цветом выделены повторяющиеся подграфы. Редакционные расстояния в этих подграфах совпадают, поэтому мы можем вычислить их только один раз и сохранить на будущее.</p>
77
<p>На рисунке цветом выделены повторяющиеся подграфы. Редакционные расстояния в этих подграфах совпадают, поэтому мы можем вычислить их только один раз и сохранить на будущее.</p>
78
<p>Этот подход похож на кеширование. При кэшировании программа запоминает результат чтения с диска, скачивания по сети или других длительных операций.</p>
78
<p>Этот подход похож на кеширование. При кэшировании программа запоминает результат чтения с диска, скачивания по сети или других длительных операций.</p>
79
<p>Здесь мы запоминаем результат длительных вычислений. Кеширование вычислений программисты называют<strong>мемоизацией</strong>. Этот термин произошел от слова<em>memory</em>, которое переводится как "память".</p>
79
<p>Здесь мы запоминаем результат длительных вычислений. Кеширование вычислений программисты называют<strong>мемоизацией</strong>. Этот термин произошел от слова<em>memory</em>, которое переводится как "память".</p>
80
<p>Посмотрим, как это выглядит в коде:</p>
80
<p>Посмотрим, как это выглядит в коде:</p>
81
<p>Код стал сложнее буквально на несколько строк. В начале функции fastDistance() мы создаем хеш-таблицу map. В качестве ключа используем строку, в которой хранятся переменные index1 и index2 - они однозначно задают пару слов.</p>
81
<p>Код стал сложнее буквально на несколько строк. В начале функции fastDistance() мы создаем хеш-таблицу map. В качестве ключа используем строку, в которой хранятся переменные index1 и index2 - они однозначно задают пару слов.</p>
82
<p>Если ключ есть в хеш-таблице, мы извлекаем из нее редакционное расстояние, которое вычислили на одном из предыдущих шагов. Если ключа нет, вычисляем редакционное расстояние и помещаем его в хеш.</p>
82
<p>Если ключ есть в хеш-таблице, мы извлекаем из нее редакционное расстояние, которое вычислили на одном из предыдущих шагов. Если ключа нет, вычисляем редакционное расстояние и помещаем его в хеш.</p>
83
<p>Насколько быстрее стала работать наша программа? Мы добились колоссальной разницы - новая реализация строит всего 24 узла вместо 520. Алгоритмическая сложность первого алгоритма равна O(3N+M), а второго - O(NM).</p>
83
<p>Насколько быстрее стала работать наша программа? Мы добились колоссальной разницы - новая реализация строит всего 24 узла вместо 520. Алгоритмическая сложность первого алгоритма равна O(3N+M), а второго - O(NM).</p>
84
<p>Этот способ решения задач на графах называют<strong>динамическим программированием</strong>. Его можно использовать, если в графе решений встречаются похожие подграфы, которые мы можем мемоизировать.</p>
84
<p>Этот способ решения задач на графах называют<strong>динамическим программированием</strong>. Его можно использовать, если в графе решений встречаются похожие подграфы, которые мы можем мемоизировать.</p>
85
<p>Но это не самое простое решение задачи о редакционном расстоянии.</p>
85
<p>Но это не самое простое решение задачи о редакционном расстоянии.</p>
86
<h2>Изучаем алгоритм Левенштейна</h2>
86
<h2>Изучаем алгоритм Левенштейна</h2>
87
<p>Самое простое решение этой задачи придумал советский математик Владимир Левенштейн в 1965 году. В его честь это решение называется<strong>алгоритм Левенштейна</strong>или<strong>функция Левенштейна</strong>. Он отказался от явного построения графа и использовал вместо него двумерный массив, который одновременно нужен и для мемоизации.</p>
87
<p>Самое простое решение этой задачи придумал советский математик Владимир Левенштейн в 1965 году. В его честь это решение называется<strong>алгоритм Левенштейна</strong>или<strong>функция Левенштейна</strong>. Он отказался от явного построения графа и использовал вместо него двумерный массив, который одновременно нужен и для мемоизации.</p>
88
<p>Для начала посмотрим на вырожденные матрицы. Они возникают, если одно из слов - пустое. Чтобы превратить одно пустое слово в другое, нужно ноль операций. Чтобы превратить одну букву В в пустое слово, нужна одна операция - удаление буквы. Для слова ВХ потребуется 2 удаления, для ВХО - 3, а для ВХОД - 4.</p>
88
<p>Для начала посмотрим на вырожденные матрицы. Они возникают, если одно из слов - пустое. Чтобы превратить одно пустое слово в другое, нужно ноль операций. Чтобы превратить одну букву В в пустое слово, нужна одна операция - удаление буквы. Для слова ВХ потребуется 2 удаления, для ВХО - 3, а для ВХОД - 4.</p>
89
<p>На рисунке показана матрица, которая соответствует этим удалениям:</p>
89
<p>На рисунке показана матрица, которая соответствует этим удалениям:</p>
90
<p>Движение по матрице на одну ячейку вниз означает одно удаление. Похожая матрица показывает, сколько вставок поможет превратить пустое слово в слова В, ВД, ВДО и ВДОХ:</p>
90
<p>Движение по матрице на одну ячейку вниз означает одно удаление. Похожая матрица показывает, сколько вставок поможет превратить пустое слово в слова В, ВД, ВДО и ВДОХ:</p>
91
<p>Движение по матрице на одну ячейку вправо означает одну вставку. Чтобы посчитать редакционное расстояние между словами ВХОД и ВДОХ, потребуется такая матрица:</p>
91
<p>Движение по матрице на одну ячейку вправо означает одну вставку. Чтобы посчитать редакционное расстояние между словами ВХОД и ВДОХ, потребуется такая матрица:</p>
92
<p>Первую строку и первую колонку матрицы заполняем последовательными числами от 0 до длины слова. Высота матрицы на единицу больше первого слова, а ширина - на единицу больше второго. Лишние строка и столбец помогают справиться с вырожденным случаем, когда одно из слов - пустое.</p>
92
<p>Первую строку и первую колонку матрицы заполняем последовательными числами от 0 до длины слова. Высота матрицы на единицу больше первого слова, а ширина - на единицу больше второго. Лишние строка и столбец помогают справиться с вырожденным случаем, когда одно из слов - пустое.</p>
93
<p>Начинаем заполнять матрицу с верхней левой пустой ячейки, которая лежит на пересечении строки В и столбца В:</p>
93
<p>Начинаем заполнять матрицу с верхней левой пустой ячейки, которая лежит на пересечении строки В и столбца В:</p>
94
<p>Посмотрим, какие ячейки находятся вокруг строки В и столбца В:</p>
94
<p>Посмотрим, какие ячейки находятся вокруг строки В и столбца В:</p>
95
<ul><li>Слева находится розовая ячейка с весом 1. Движение вправо означает вставку, стоимость вставки равна единице, поэтому суммарное редакционное расстояние равно 2</li>
95
<ul><li>Слева находится розовая ячейка с весом 1. Движение вправо означает вставку, стоимость вставки равна единице, поэтому суммарное редакционное расстояние равно 2</li>
96
<li>Сверху находится зеленая ячейка с весом 1. Движение вниз означает удаление, стоимость которого равна 1 и суммарное редакционное расстояние равно 2</li>
96
<li>Сверху находится зеленая ячейка с весом 1. Движение вниз означает удаление, стоимость которого равна 1 и суммарное редакционное расстояние равно 2</li>
97
<li>Слева-сверху от нее находится желтая ячейка с весом 0. Движение по диагонали направо и вниз означает удаление и вставку одновременно, то есть, по сути, замену буквы. Стоимость замены зависит от того, совпадают ли соответствующие буквы в слове. Здесь они совпадают, поэтому стоимость замены равна 0</li>
97
<li>Слева-сверху от нее находится желтая ячейка с весом 0. Движение по диагонали направо и вниз означает удаление и вставку одновременно, то есть, по сути, замену буквы. Стоимость замены зависит от того, совпадают ли соответствующие буквы в слове. Здесь они совпадают, поэтому стоимость замены равна 0</li>
98
</ul><p>Мы получили три числа - 2, 2 и 0. Выбираем наименьшее число 0 и записываем в ячейку:</p>
98
</ul><p>Мы получили три числа - 2, 2 и 0. Выбираем наименьшее число 0 и записываем в ячейку:</p>
99
<p>Мы определили, что редакционное расстояние между словами В и В равно 0.</p>
99
<p>Мы определили, что редакционное расстояние между словами В и В равно 0.</p>
100
<p>Алгоритм очень похож на графовый. Заполняя ячейку, мы учитываем трех соседей:</p>
100
<p>Алгоритм очень похож на графовый. Заполняя ячейку, мы учитываем трех соседей:</p>
101
<ul><li>Сверху (удаление)</li>
101
<ul><li>Сверху (удаление)</li>
102
<li>Слева (вставка)</li>
102
<li>Слева (вставка)</li>
103
<li>Слева-сверху (замена)</li>
103
<li>Слева-сверху (замена)</li>
104
</ul><p>При удалении и вставке мы всегда добавляем единицу, а при замене - только в том случае, если буквы различаются.</p>
104
</ul><p>При удалении и вставке мы всегда добавляем единицу, а при замене - только в том случае, если буквы различаются.</p>
105
<p>Следуя алгоритму, заполним вторую и третью строки:</p>
105
<p>Следуя алгоритму, заполним вторую и третью строки:</p>
106
<p>На пересечении строки Х и столбца О мы видим значение 2. Это означает, что редакционное расстояние между словами ВХ и ВДО равно 2 - одна замена и одна вставка.</p>
106
<p>На пересечении строки Х и столбца О мы видим значение 2. Это означает, что редакционное расстояние между словами ВХ и ВДО равно 2 - одна замена и одна вставка.</p>
107
<p>Таким образом, мы строим матрицу и вычисляем редакционное расстояние между всеми промежуточными более короткими словами.</p>
107
<p>Таким образом, мы строим матрицу и вычисляем редакционное расстояние между всеми промежуточными более короткими словами.</p>
108
<p>Заполним оставшиеся строки:</p>
108
<p>Заполним оставшиеся строки:</p>
109
<p>Значение в правом нижнем углу в желтой ячейке - это есть искомое редакционное расстояние между словами ВХОД и ВДОХ.</p>
109
<p>Значение в правом нижнем углу в желтой ячейке - это есть искомое редакционное расстояние между словами ВХОД и ВДОХ.</p>
110
<h2>Реализуем алгоритм Левенштейна</h2>
110
<h2>Реализуем алгоритм Левенштейна</h2>
111
<p>Посмотрим, как выглядит реализация в коде:</p>
111
<p>Посмотрим, как выглядит реализация в коде:</p>
112
<p>Разберем ее подробнее. Сначала мы создаем матрицу и добавляем в нее первую строку. Заполняем ее числами 0, 1, 2 и так далее:</p>
112
<p>Разберем ее подробнее. Сначала мы создаем матрицу и добавляем в нее первую строку. Заполняем ее числами 0, 1, 2 и так далее:</p>
113
<p>Далее добавляем оставшиеся строки, чтобы высота матрицы была на единицу больше первого слова. В первой ячейке каждой строки проставляем числа 1, 2, 3 и так далее:</p>
113
<p>Далее добавляем оставшиеся строки, чтобы высота матрицы была на единицу больше первого слова. В первой ячейке каждой строки проставляем числа 1, 2, 3 и так далее:</p>
114
<p>Организуем вложенный цикл. Во внешнем операторе for перебираем строки сверху вниз, а во внутреннем - ячейки в строке слева направо:</p>
114
<p>Организуем вложенный цикл. Во внешнем операторе for перебираем строки сверху вниз, а во внутреннем - ячейки в строке слева направо:</p>
115
<p>Во вложенном цикле есть три действия:</p>
115
<p>Во вложенном цикле есть три действия:</p>
116
<ul><li>Вставка через переменную ins</li>
116
<ul><li>Вставка через переменную ins</li>
117
<li>Удаление через del</li>
117
<li>Удаление через del</li>
118
<li>Замена через sub</li>
118
<li>Замена через sub</li>
119
</ul><p>Подсчитываем стоимость этих действий:</p>
119
</ul><p>Подсчитываем стоимость этих действий:</p>
120
<ul><li>Вставка на единицу больше значения в ячейке слева</li>
120
<ul><li>Вставка на единицу больше значения в ячейке слева</li>
121
<li>Удаление на единицу больше значения в ячейке сверху</li>
121
<li>Удаление на единицу больше значения в ячейке сверху</li>
122
<li>Замена равна значению в ячейке слева-сверху, если буквы совпадают. Если буквы не совпадают, замена также на единицу больше</li>
122
<li>Замена равна значению в ячейке слева-сверху, если буквы совпадают. Если буквы не совпадают, замена также на единицу больше</li>
123
</ul><p>В конце мы возвращаем значение ячейки в правом нижнем углу матрицы:</p>
123
</ul><p>В конце мы возвращаем значение ячейки в правом нижнем углу матрицы:</p>
124
<p>Этот алгоритм проще явного перебора узлов. Мы избавились от рекурсии и использовали обычный двумерный массив в качестве основной структуры данных.</p>
124
<p>Этот алгоритм проще явного перебора узлов. Мы избавились от рекурсии и использовали обычный двумерный массив в качестве основной структуры данных.</p>
125
<p>Сложность этого алгоритма такая же, как и в случае с мемоизацией - то есть O(NM). Сложность по памяти также равна O(NM), поскольку мы храним матрицу размера NM.</p>
125
<p>Сложность этого алгоритма такая же, как и в случае с мемоизацией - то есть O(NM). Сложность по памяти также равна O(NM), поскольку мы храним матрицу размера NM.</p>
126
<p>Однако мы можем хранить не всю матрицу, а только одну строку, потому что для вычислений нам нужна только предыдущая строка. Конечно, такая реализация требует аккуратности, поскольку старые значения в строке перезаписываются новыми. Поэтому все должно быть сделано в правильном порядке.</p>
126
<p>Однако мы можем хранить не всю матрицу, а только одну строку, потому что для вычислений нам нужна только предыдущая строка. Конечно, такая реализация требует аккуратности, поскольку старые значения в строке перезаписываются новыми. Поэтому все должно быть сделано в правильном порядке.</p>
127
<h2>Выводы</h2>
127
<h2>Выводы</h2>
128
<p>Повторим ключевые выводы этого урока:</p>
128
<p>Повторим ключевые выводы этого урока:</p>
129
<ul><li>Для вычисления редакционного расстояния можно построить граф решений</li>
129
<ul><li>Для вычисления редакционного расстояния можно построить граф решений</li>
130
<li>Размер такого графа и время работы алгоритма немного меньше, чем 3N+M, где N - длина первого слова, а M - длина второго слова</li>
130
<li>Размер такого графа и время работы алгоритма немного меньше, чем 3N+M, где N - длина первого слова, а M - длина второго слова</li>
131
<li>Алгоритм можно кардинально ускорить, если использовать динамическое программирование</li>
131
<li>Алгоритм можно кардинально ускорить, если использовать динамическое программирование</li>
132
<li>Нужно избавиться от повторных промежуточных вычислений, сохраняя каждое из них в хеш-таблицы</li>
132
<li>Нужно избавиться от повторных промежуточных вычислений, сохраняя каждое из них в хеш-таблицы</li>
133
<li>У первого алгоритма экспоненциальная алгоритмическая сложность, а второго - квадратичная</li>
133
<li>У первого алгоритма экспоненциальная алгоритмическая сложность, а второго - квадратичная</li>
134
<li>Можно еще более упростить реализацию, избавившись от явного представления узлов графа и хеш-таблицы</li>
134
<li>Можно еще более упростить реализацию, избавившись от явного представления узлов графа и хеш-таблицы</li>
135
<li>Самый эффективный алгоритм - это алгоритм Левенштейна, он имеет квадратичную алгоритмическую сложность</li>
135
<li>Самый эффективный алгоритм - это алгоритм Левенштейна, он имеет квадратичную алгоритмическую сложность</li>
136
</ul>
136
</ul>