Расстояние Левенштейна

Эта статья находится на начальном уровне проработки, в одной из её версий выборочно используется текст из источника, распространяемого под свободной лицензией
Материал из энциклопедии Руниверсалис

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

Впервые задачу поставил в 1965 году советский математик Владимир Левенштейн при изучении последовательностей [math]\displaystyle{ 0-1 }[/math][1], впоследствии более общую задачу для произвольного алфавита связали с его именем. Большой вклад в изучение вопроса внёс Дэн Гасфилд[2].

Применение

Расстояние Левенштейна и его обобщения активно применяется:

  • для исправления ошибок в слове (в поисковых системах, базах данных, при вводе текста, при автоматическом распознавании отсканированного текста или речи).
  • для сравнения текстовых файлов утилитой diff и ей подобными. Здесь роль «символов» играют строки, а роль «строк» — файлы.
  • в биоинформатике для сравнения генов, хромосом и белков.

С точки зрения приложений определение расстояния между словами или текстовыми полями по Левенштейну обладает следующими недостатками:

  1. При перестановке местами слов или частей слов получаются сравнительно большие расстояния;
  2. Расстояния между совершенно разными короткими словами оказываются небольшими, в то время как расстояния между очень похожими длинными словами оказываются значительными.

Редакционное предписание

Редакционным предписанием называется последовательность действий, необходимых для получения из первой строки второй кратчайшим образом. Обычно действия обозначаются так: 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

Найти только расстояние Левенштейна — более простая задача, чем найти ещё и редакционное предписание (подробнее см. ниже).

Обобщения

Разные цены операций

Цены операций могут зависеть от вида операции (вставка, удаление, замена) и/или от участвующих в ней символов, отражая разную вероятность мутаций в биологии[3], разную вероятность разных ошибок при вводе текста и т. д. В общем случае:

  • 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 % ошибок при наборе текста человеком являются транспозициями. Кроме того, расстояние Дамерау — Левенштейна используется и в биоинформатике.

Формула

Здесь и далее считается, что элементы строк нумеруются с первого, как принято в математике, а не с нулевого, как принято во многих языках программирования.

Пусть [math]\displaystyle{ S_1 }[/math] и [math]\displaystyle{ S_2 }[/math] — две строки (длиной [math]\displaystyle{ M }[/math] и [math]\displaystyle{ N }[/math] соответственно) над некоторым алфавитом, тогда редакционное расстояние (расстояние Левенштейна) [math]\displaystyle{ {\rm d}(S_1, S_2) }[/math] можно подсчитать по следующей рекуррентной формуле

[math]\displaystyle{ \ {\rm d}(S_1, S_2) = D(M,N) }[/math] , где

[math]\displaystyle{ D(i, j) = \left\{\begin{array}{llcl} 0,&&&i = 0,\ j = 0\\ i,&&&j = 0,\ i \gt 0\\ j,&&&i = 0,\ j \gt 0\\ \min\{\\ &D(i, j - 1) + 1,\\ &D(i - 1, j) + 1,&&j \gt 0,\ i \gt 0\\ &D(i - 1, j - 1) + {\rm m}(S_1[i], S_2[j])\\ \} \end{array}\right. }[/math],

где [math]\displaystyle{ {\rm m}(a,b) }[/math] равна нулю, если [math]\displaystyle{ a = b }[/math] и единице в противном случае; [math]\displaystyle{ \min\{\,a, b, c\,\} }[/math] возвращает наименьший из аргументов.

Здесь шаг по [math]\displaystyle{ i }[/math] символизирует удаление (D) из первой строки, по [math]\displaystyle{ j }[/math] — вставку (I) в первую строку, а шаг по обоим индексам символизирует замену символа (R) или отсутствие изменений (M).

Очевидно, справедливы следующие утверждения:

  • [math]\displaystyle{ {\rm{d}}(S_1,S_2) \geqslant \bigl| |S_1| - |S_2| \bigr| }[/math]
  • [math]\displaystyle{ {\rm{d}}(S_1,S_2) \leqslant \max\bigl( |S_1| , |S_2| \bigr) }[/math]
  • [math]\displaystyle{ \mathop{\rm{d}}(S_1,S_2) = 0 \Leftrightarrow S_1 = S_2 }[/math]
Пример работы алгоритма.
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

Доказательство

Рассмотрим формулу более подробно. Очевидно, что редакционное расстояние между двумя пустыми строками равно нулю. Так же очевидно то, что чтобы получить пустую строку из строки длиной [math]\displaystyle{ i }[/math], нужно совершить [math]\displaystyle{ i }[/math] операций удаления, а чтобы получить строку длиной [math]\displaystyle{ j }[/math] из пустой, нужно произвести [math]\displaystyle{ j }[/math] операций вставки.

Осталось рассмотреть нетривиальный случай, когда обе строки непусты.

Для начала заметим, что в оптимальной последовательности операций их можно произвольно менять местами. В самом деле, рассмотрим две последовательные операции:

  • Две замены одного и того же символа — неоптимально (если мы заменили x на y, потом — y на z, выгоднее было сразу заменить x на z).
  • Две замены разных символов можно менять местами
  • Два стирания или две вставки можно менять местами
  • Вставка символа с его последующим стиранием — неоптимально (можно их обе отменить)
  • Стирание и вставку разных символов можно менять местами
  • Вставка символа с его последующей заменой — неоптимально (излишняя замена)
  • Вставка символа и замена другого символа меняются местами
  • Замена символа с его последующим стиранием — неоптимально (излишняя замена)
  • Стирание символа и замена другого символа меняются местами

Пусть [math]\displaystyle{ S_1 }[/math] кончается на символ «a», [math]\displaystyle{ S_2 }[/math] кончается на символ «b». Есть три варианта:

  1. Символ «а», на который кончается [math]\displaystyle{ S_1 }[/math], в какой-то момент был стёрт. Сделаем это стирание первой операцией. Тогда мы стёрли символ «a», после чего превратили первые [math]\displaystyle{ i-1 }[/math] символов [math]\displaystyle{ S_1 }[/math] в [math]\displaystyle{ S_2 }[/math] (на что потребовалось [math]\displaystyle{ D(i-1,\ j) }[/math] операций), значит, всего потребовалось [math]\displaystyle{ D(i-1,\ j)+1 }[/math] операций
  2. Символ «b», на который кончается [math]\displaystyle{ S_2 }[/math], в какой-то момент был добавлен. Сделаем это добавление последней операцией. Мы превратили [math]\displaystyle{ S_1 }[/math] в первые [math]\displaystyle{ j-1 }[/math] символов [math]\displaystyle{ S_2 }[/math], после чего добавили «b». Аналогично предыдущему случаю, потребовалось [math]\displaystyle{ D(i,\ j-1)+1 }[/math] операций.
  3. Оба предыдущих утверждения неверны. Если мы добавляли символы справа от финального «a», то, чтобы сделать последним символом «b», мы должны были или в какой-то момент добавить его (но тогда утверждение 2 было бы верно), либо заменить на него один из этих добавленных символов (что тоже невозможно, потому что добавление символа с его последующей заменой неоптимально). Значит, символов справа от финального «a» мы не добавляли. Самого финального «a» мы не стирали, поскольку утверждение 1 неверно. Значит, единственный способ изменения последнего символа — его замена. Заменять его 2 или больше раз неоптимально. Значит,
    1. Если [math]\displaystyle{ a=b }[/math], мы последний символ не меняли. Поскольку мы его также не стирали и не приписывали ничего справа от него, он не влиял на наши действия, и, значит, мы выполнили [math]\displaystyle{ D(i-1,\ j-1) }[/math] операций.
    2. Если [math]\displaystyle{ a\ne b }[/math], мы последний символ меняли один раз. Сделаем эту замену первой. В дальнейшем, аналогично предыдущему случаю, мы должны выполнить [math]\displaystyle{ D(i-1,\ j-1) }[/math] операций, значит, всего потребуется [math]\displaystyle{ D(i-1,\ j-1)+1 }[/math] операций.

Алгоритм Вагнера — Фишера

Для нахождения кратчайшего расстояния необходимо вычислить матрицу 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) в левый верхний, на каждом шаге ища минимальное из трёх значений:

  • если минимально ([math]\displaystyle{ D(i - 1, j) }[/math]+ цена удаления символа S1[i]), добавляем удаление символа S1[i] и идём в (i-1, j)
  • если минимально ([math]\displaystyle{ D(i, j - 1) }[/math]+ цена вставки символа S2[j]), добавляем вставку символа S2[j] и идём в (i, j-1)
  • если минимально ([math]\displaystyle{ D(i - 1, j - 1) }[/math]+ цена замены символа S1[i] на символ S2[j]), добавляем замену S1[i] на S2[j] (если они не равны; иначе ничего не добавляем), после чего идём в (i-1, j-1)

Здесь (i, j) — клетка матрицы, в которой мы находимся на данном шаге. Если минимальны два из трёх значений (или равны все три), это означает, что есть 2 или 3 равноценных редакционных предписания.

Этот алгоритм называется алгоритмом Вагнера — Фишера. Он предложен Р. Вагнером (R. A. Wagner) и М. Фишером (M. J. Fischer) в 1974 году.[4]

Память

Алгоритм в виде, описанном выше, требует [math]\displaystyle{ \Theta(M \cdot N) }[/math] операций и такую же память. Последнее может быть неприятным: так, для сравнения файлов длиной в 105 строк потребуется около 40 гигабайт памяти.

Если требуется только расстояние, легко уменьшить требуемую память до [math]\displaystyle{ \Theta(\min\{\,M,N\,\}) }[/math]. Для этого надо учесть, что после вычисления любой строки предыдущая строка больше не нужна. Более того, после вычисления 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)

Если требуется редакционное предписание, экономия памяти усложняется.

Для того, чтобы обеспечить время [math]\displaystyle{ \Theta(M \cdot N) }[/math] при памяти [math]\displaystyle{ \Theta(\min\{\,M,N\,\}) }[/math], определим матрицу E минимальных расстояний между суффиксами строк, то есть E(i, j) — расстояние между последними i символами [math]\displaystyle{ S_1 }[/math] и последними j символами [math]\displaystyle{ S_2 }[/math]. Очевидно, матрицу E можно вычислить аналогично матрице D, и так же быстро.

Теперь опишем алгоритм, считая, что [math]\displaystyle{ S_2 }[/math] — кратчайшая из двух строк.

  • Если длина одной из строк (или обеих) не больше 1, задача тривиальна. Если нет, выполним следующие шаги.
  • Разделим строку [math]\displaystyle{ S_1 }[/math] на две подстроки длиной [math]\displaystyle{ M/2 }[/math]. (Если M нечётно, то длины подстрок будут [math]\displaystyle{ (M-1)/2 }[/math] и [math]\displaystyle{ (M+1)/2 }[/math].) Обозначим подстроки [math]\displaystyle{ S_1^- }[/math] и [math]\displaystyle{ S_1^+ }[/math].
  • Для [math]\displaystyle{ S_1^- }[/math] вычислим последнюю строку матрицы D, а для [math]\displaystyle{ S_1^+ }[/math] — последнюю строку матрицы E.
  • Найдём i такое, что [math]\displaystyle{ D(|S_1^-|, i) + E(|S_1^+|, N-i) }[/math] минимально. Здесь D и Е — матрицы из предыдущего шага, но мы используем только их последние строки. Таким образом, мы нашли разбиение [math]\displaystyle{ S_2 }[/math] на две подстроки, минимизирующее сумму расстояния левой половины [math]\displaystyle{ S_1 }[/math] до левой части [math]\displaystyle{ S_2 }[/math] и расстояния правой половины [math]\displaystyle{ S_1 }[/math] до правой части [math]\displaystyle{ S_2 }[/math]. Следовательно, левая подстрока [math]\displaystyle{ S_2 }[/math] соответствует левой половине [math]\displaystyle{ S_1 }[/math], а правая — правой.
  • Рекурсивно ищем редакционное предписание, превращающее [math]\displaystyle{ S_1^- }[/math] в левую часть [math]\displaystyle{ S_2 }[/math] (то есть в подстроку [math]\displaystyle{ S2[1...i] }[/math])
  • Рекурсивно ищем редакционное предписание, превращающее [math]\displaystyle{ S_1^+ }[/math] в правую часть [math]\displaystyle{ S_2 }[/math] (то есть в подстроку [math]\displaystyle{ S2[i+1...N] }[/math]).
  • Объединяем оба редакционных предписания.[5]

Время выполнения удовлетворяет (с точностью до умножения на константу) условию

[math]\displaystyle{ T(M,N)=MN+T(M/2,N')+T(M/2,N-N'),\ 0\le N'\le N }[/math],

откуда следует (доказывается индукцией по M)

[math]\displaystyle{ T(M,N) \le 2MN }[/math]

следовательно

[math]\displaystyle{ T(M,N) = \Theta(M \cdot N) }[/math]

Требуемая память пропорциональна [math]\displaystyle{ N+N/2+N/4+...=2N }[/math]

Кроме того, есть алгоритмы, экономящие память за счёт существенного замедления, например, время становится кубическим, а не квадратичным, по длине строк.

Примечания

  1. В. И. Левенштейн. Двоичные коды с исправлением выпадений, вставок и замещений символов. Доклады Академий Наук СССР, 1965. 163.4:845-848.
  2. Гасфилд. Строки, деревья и последовательности в алгоритмах. Информатика и вычислительная биология. Невский Диалект БВХ-Петербург, 2003.
  3. См., например: http://www.medlit.ru/medrus/mg/mg080237.htm Архивная копия от 8 марта 2012 на Wayback Machine
  4. R. A. Wagner, M. J. Fischer. The string-to-string correction problem. J. ACM 21 1 (1974). P. 168—173
  5. При этом во втором редакционном предписании нужно увеличить номера символов первой строки на [math]\displaystyle{ |S_1^-| }[/math], а второй строки на [math]\displaystyle{ i }[/math], поскольку теперь они отсчитываются с начала строк, a не с их середины.