HTML Diff
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>