Расстояние Левенштейна
(
редакционное расстояние
,
дистанция редактирования
) —
метрика
, измеряющая по модулю разность между двумя последовательностями символов. Она определяется как минимальное количество односимвольных операций (а именно вставки, удаления, замены), необходимых для превращения одной последовательности символов в другую. В общем случае, операциям, используемым в этом преобразовании, можно назначить разные цены. Широко используется в
теории информации
и
компьютерной лингвистике
.
Впервые задачу поставил в
1965 году
советский математик
Владимир Левенштейн
при изучении последовательностей
, впоследствии более общую задачу для произвольного алфавита связали с его именем. Большой вклад в изучение вопроса внёс Дэн Гасфилд
.
Применение
Расстояние Левенштейна и его обобщения активно применяется:
-
для исправления ошибок в слове (в поисковых системах, базах данных, при вводе текста, при автоматическом распознавании отсканированного текста или речи).
-
для сравнения текстовых файлов утилитой
diff
и ей подобными. Здесь роль «символов» играют строки, а роль «строк» — файлы.
-
в биоинформатике для сравнения
генов
,
хромосом
и
белков
.
С точки зрения приложений определение расстояния между словами или текстовыми полями по Левенштейну обладает следующими недостатками:
-
При перестановке местами слов или частей слов получаются сравнительно большие расстояния;
-
Расстояния между совершенно разными короткими словами оказываются небольшими, в то время как расстояния между очень похожими длинными словами оказываются значительными.
Редакционное предписание
Редакционным предписанием
называется последовательность действий, необходимых для получения из первой строки второй кратчайшим образом. Обычно действия обозначаются так:
D
(
англ.
delete
) — удалить,
I
(англ. insert) — вставить,
R
(
replace
) — заменить,
M
(
match
) — совпадение.
Например, для 2 строк «CONNECT» и «CONEHEAD» можно построить следующую таблицу преобразований:
M
|
M
|
M
|
R
|
I
|
M
|
R
|
R
|
C
|
O
|
N
|
N
|
|
E
|
C
|
T
|
C
|
O
|
N
|
E
|
H
|
E
|
A
|
D
|
Найти только расстояние Левенштейна — более простая задача, чем найти ещё и редакционное предписание (подробнее см. ниже).
Обобщения
Разные цены операций
Цены операций могут зависеть от вида операции (вставка, удаление, замена) и/или от участвующих в ней символов, отражая разную вероятность мутаций в биологии
, разную вероятность разных ошибок при вводе текста и т. д. В общем случае:
-
w(a, b) — цена замены символа a на символ b
-
w(ε, b) — цена вставки символа b
-
w(a, ε) — цена удаления символа a
Необходимо найти последовательность замен, минимизирующую суммарную цену. Расстояние Левенштейна является частным случаем этой задачи при
-
w(a, а) = 0
-
w(a, b) = 1 при a≠b
-
w(ε, b) = 1
-
w(a, ε) = 1
Как частный случай, так и задачу для произвольных w, решает алгоритм Вагнера — Фишера, приведённый ниже. Здесь и ниже мы считаем, что все w неотрицательны, и действует
неравенство треугольника
: замена двух последовательных операций одной не увеличит общую цену (например, замена символа x на y, а потом y на z не лучше, чем сразу x на z).
Транспозиция
Если к списку разрешённых операций добавить транспозицию (два соседних символа меняются местами), получается
расстояние Дамерау — Левенштейна
. Для неё также существует алгоритм, требующий O(MN) операций. Дамерау показал, что 80 % ошибок при наборе текста человеком являются транспозициями. Кроме того, расстояние Дамерау — Левенштейна используется и в биоинформатике.
Формула
Здесь и далее считается, что элементы строк нумеруются с первого, как принято в математике, а не с нулевого, как принято во многих языках программирования.
Пусть
и
— две строки (длиной
и
соответственно) над некоторым
алфавитом
, тогда редакционное расстояние (расстояние Левенштейна)
можно подсчитать по следующей
рекуррентной формуле
, где
,
где
равна нулю, если
и единице в противном случае;
возвращает наименьший из аргументов.
Здесь шаг по
символизирует удаление (D) из первой строки, по
— вставку (I) в первую строку, а шаг по обоим индексам символизирует замену символа (R) или отсутствие изменений (M).
Очевидно, справедливы следующие утверждения:
-
-
-
Пример работы алгоритма.
|
|
P
|
O
|
L
|
Y
|
N
|
O
|
M
|
I
|
A
|
L
|
|
0
|
1
|
2
|
3
|
4
|
5
|
6
|
7
|
8
|
9
|
10
|
E
|
1
|
1
|
2
|
3
|
4
|
5
|
6
|
7
|
8
|
9
|
10
|
X
|
2
|
2
|
2
|
3
|
4
|
5
|
6
|
7
|
8
|
9
|
10
|
P
|
3
|
2
|
3
|
3
|
4
|
5
|
6
|
7
|
8
|
9
|
10
|
O
|
4
|
3
|
2
|
3
|
4
|
5
|
5
|
6
|
7
|
8
|
9
|
N
|
5
|
4
|
3
|
3
|
4
|
4
|
5
|
6
|
7
|
8
|
9
|
E
|
6
|
5
|
4
|
4
|
4
|
5
|
5
|
6
|
7
|
8
|
9
|
N
|
7
|
6
|
5
|
5
|
5
|
4
|
5
|
6
|
7
|
8
|
9
|
T
|
8
|
7
|
6
|
6
|
6
|
5
|
5
|
6
|
7
|
8
|
9
|
I
|
9
|
8
|
7
|
7
|
7
|
6
|
6
|
6
|
6
|
7
|
8
|
A
|
10
|
9
|
8
|
8
|
8
|
7
|
7
|
7
|
7
|
6
|
7
|
L
|
11
|
10
|
9
|
8
|
9
|
8
|
8
|
8
|
8
|
7
|
6
|
Доказательство
Рассмотрим формулу более подробно. Очевидно, что редакционное расстояние между двумя пустыми строками равно нулю. Так же очевидно то, что чтобы получить пустую строку из строки длиной
, нужно совершить
операций удаления, а чтобы получить строку длиной
из пустой, нужно произвести
операций вставки.
Осталось рассмотреть нетривиальный случай, когда обе строки непусты.
Для начала заметим, что в оптимальной последовательности операций их можно произвольно менять местами. В самом деле, рассмотрим две последовательные операции:
-
Две замены одного и того же символа — неоптимально (если мы заменили x на y, потом — y на z, выгоднее было сразу заменить x на z).
-
Две замены разных символов можно менять местами
-
Два стирания или две вставки можно менять местами
-
Вставка символа с его последующим стиранием — неоптимально (можно их обе отменить)
-
Стирание и вставку разных символов можно менять местами
-
Вставка символа с его последующей заменой — неоптимально (излишняя замена)
-
Вставка символа и замена другого символа меняются местами
-
Замена символа с его последующим стиранием — неоптимально (излишняя замена)
-
Стирание символа и замена другого символа меняются местами
Пусть
кончается на символ «a»,
кончается на символ «b». Есть три варианта:
-
Символ «а», на который кончается
, в какой-то момент был стёрт. Сделаем это стирание первой операцией. Тогда мы стёрли символ «a», после чего превратили первые
символов
в
(на что потребовалось
операций), значит, всего потребовалось
операций
-
Символ «b», на который кончается
, в какой-то момент был добавлен. Сделаем это добавление последней операцией. Мы превратили
в первые
символов
, после чего добавили «b». Аналогично предыдущему случаю, потребовалось
операций.
-
Оба предыдущих утверждения неверны. Если мы добавляли символы справа от финального «a», то, чтобы сделать последним символом «b», мы должны были или в какой-то момент добавить его (но тогда утверждение 2 было бы верно), либо заменить на него один из этих добавленных символов (что тоже невозможно, потому что добавление символа с его последующей заменой неоптимально). Значит, символов справа от финального «a» мы не добавляли. Самого финального «a» мы не стирали, поскольку утверждение 1 неверно. Значит, единственный способ изменения последнего символа — его замена. Заменять его 2 или больше раз неоптимально. Значит,
-
Если
, мы последний символ не меняли. Поскольку мы его также не стирали и не приписывали ничего справа от него, он не влиял на наши действия, и, значит, мы выполнили
операций.
-
Если
, мы последний символ меняли один раз. Сделаем эту замену первой. В дальнейшем, аналогично предыдущему случаю, мы должны выполнить
операций, значит, всего потребуется
операций.
Алгоритм Вагнера — Фишера
Для нахождения кратчайшего расстояния необходимо вычислить матрицу D, используя
. Её можно вычислять как по строкам, так и по столбцам. Псевдокод алгоритма:
для всех i от 0 до M
для всех j от 0 до N
вычислить D(i, j)
вернуть D(M, N)
Или в более развёрнутом виде, и при произвольных ценах замен, вставок и удалений:
D(0, 0) = 0
для всех j от 1 до N
D(0, j) = D(0, j - 1) + цена вставки символа S2[j]
для всех i от 1 до M
D(i, 0) = D(i - 1, 0) + цена удаления символа S1[i]
для всех j от 1 до N
D(i, j) = min{
D(i - 1, j) + цена удаления символа S1[i],
D(i, j - 1) + цена вставки символа S2[j],
D(i - 1, j - 1) + цена замены символа S1[i] на символ S2[j]
}
вернуть D(M, N)
(Напоминаем, что элементы строк нумеруются с
первого
, а не с нулевого.)
Для восстановления редакционного предписания требуется вычислить матрицу D, после чего идти из правого нижнего угла (M,N) в левый верхний, на каждом шаге ища минимальное из трёх значений:
-
если минимально (
+ цена удаления символа S1[i]), добавляем удаление символа S1[i] и идём в (i-1, j)
-
если минимально (
+ цена вставки символа S2[j]), добавляем вставку символа S2[j] и идём в (i, j-1)
-
если минимально (
+ цена замены символа S1[i] на символ S2[j]), добавляем замену S1[i] на S2[j] (если они не равны; иначе ничего не добавляем), после чего идём в (i-1, j-1)
Здесь (i, j) — клетка матрицы, в которой мы находимся на данном шаге. Если минимальны два из трёх значений (или равны все три), это означает, что есть 2 или 3 равноценных редакционных предписания.
Этот алгоритм называется алгоритмом Вагнера — Фишера. Он предложен Р. Вагнером (R. A. Wagner) и М. Фишером (M. J. Fischer) в 1974 году.
Память
Алгоритм в виде, описанном выше, требует
операций и такую же память. Последнее может быть неприятным: так, для сравнения файлов длиной в 10
5
строк потребуется около 40 гигабайт памяти.
Если требуется только расстояние, легко уменьшить требуемую память до
. Для этого надо учесть, что после вычисления любой строки предыдущая строка больше не нужна. Более того, после вычисления D(i, j) не нужны также D(i-1,0) … D(i-1,j-1). Поэтому алгоритм можно переписать как
для всех i от 0 до M
для всех j от 0 до N
вычислить D(i, j)
если i > 0
стереть строку D(i - 1)
вернуть D(M, N)
или даже
для всех i от 0 до M
для всех j от 0 до N
вычислить D(i, j)
если i > 0 и j > 0
стереть D(i - 1, j - 1)
вернуть D(M, N)
Если требуется редакционное предписание, экономия памяти усложняется.
Для того, чтобы обеспечить время
при памяти
, определим матрицу E минимальных расстояний между
суффиксами
строк, то есть E(i, j) — расстояние между последними i символами
и последними j символами
. Очевидно, матрицу E можно вычислить аналогично матрице D, и так же быстро.
Теперь опишем алгоритм, считая, что
— кратчайшая из двух строк.
-
Если длина одной из строк (или обеих) не больше 1, задача тривиальна. Если нет, выполним следующие шаги.
-
Разделим строку
на две подстроки длиной
. (Если M нечётно, то длины подстрок будут
и
.) Обозначим подстроки
и
.
-
Для
вычислим последнюю строку матрицы D, а для
— последнюю строку матрицы E.
-
Найдём i такое, что
минимально. Здесь D и Е — матрицы из предыдущего шага, но мы используем только их последние строки. Таким образом, мы нашли разбиение
на две подстроки, минимизирующее сумму расстояния левой половины
до левой части
и расстояния правой половины
до правой части
. Следовательно, левая подстрока
соответствует левой половине
, а правая — правой.
-
Рекурсивно ищем редакционное предписание, превращающее
в левую часть
(то есть в подстроку
)
-
Рекурсивно ищем редакционное предписание, превращающее
в правую часть
(то есть в подстроку
).
-
Объединяем оба редакционных предписания.
Время выполнения удовлетворяет (с точностью до умножения на константу) условию
-
,
откуда следует (доказывается индукцией по M)
-
следовательно
-
Требуемая память пропорциональна
Кроме того, есть алгоритмы, экономящие память за счёт существенного замедления, например, время становится кубическим, а не квадратичным, по длине строк.
Примечания
-
В. И. Левенштейн. Двоичные коды с исправлением выпадений, вставок и замещений символов. Доклады Академий Наук СССР, 1965. 163.4:845-848.
-
Гасфилд. Строки, деревья и последовательности в алгоритмах. Информатика и вычислительная биология. Невский Диалект БВХ-Петербург, 2003.
-
См., например:
от 8 марта 2012 на
Wayback Machine
-
R. A. Wagner, M. J. Fischer. The string-to-string correction problem.
J. ACM
21 1 (1974). P. 168—173
-
При этом во втором редакционном предписании нужно увеличить номера символов первой строки на
, а второй строки на
, поскольку теперь они отсчитываются с начала строк, a не с их середины.