Алгоритм Кнута-Морриса-Пратта

Алгоритм Кнута - Морриса - Пратта

Алгоритм Кнута-Морриса-Пратта (КМП) получает на  вход слово
         X=x[1]x[2]... x[n]
и просматривает его слева направо буква за буквой, заполняя при этом массив
натуральных чисел l[1]... l[n], где
         l[i]=длина слова l(x[1]...х[i])
(функция l определена в предыдущем пункте). Словами: l[i] есть длина
наибольшего начала слова x[1]...x[i], одновременно являющегося его концом.
    Какое отношение все это имеет к поиску подслова?
Другими словами, как использовать алгоритм КМП для определения того,
является ли слово A подсловом слова B?
Решение. Применим алгоритм КМП к слову A#B, где # - специальная буква, не
встречающаяся ни в A, ни в B. Слово A является подсловом слова B тогда и
только тогда, когда среди чисел в массиве l будет число, равное длине слова
A.
    Описать алгоритм заполнения таблицы l[1]...l[n].
Решение. Предположим, что первые i значений l[1]...l[i] уже найдены. Мы
читаем очередную букву слова (т.е. x[i+1]) и должны вычислить l[i+1].
[pic]
Другими словами, нас интересуют начала Z слова
         x[1]...x[i+1,
одновременно являющиеся его концами -из них нам надо брать самое длинное.
Откуда берутся эти начала? Каждое из них (не считая пустого) получается из
некоторого слова Z' приписыванием буквы x[i+1] . Слово Z' является началом
и
концом слова x[1]...x[i]. Однако не любое слово, являющееся началом и
концом слова x[1]...x[i], годится - надо, чтобы за ним следовала буква
x[i+1].
     Получаем такой рецепт отыскания слова Z. Рассмотрим все начала слова
x[1]...x[i], являющиеся одновременно его концами. Из них выберем подходящие
- те, за которыми идет буква x[i+1]. Из подходящих выберем самое длинное.
Приписав в его конец х[i+1], получим искомое слово Z. Теперь пора
воспользоваться сделанными нами приготовлениями и вспомнить, что все слова,
являющиеся одновременно началами и концами данного слова, можно получить
повторными применениями к нему функции l из предыдущего раздела.
    Вот что получается:
         i:=1; 1[1]:=0;
    {таблица l[1]..l[i] заполнена правильно}
    while i <> n do begin
       len:= l[i]
       {len - длина начала слова x[1]..x[i], которое является
       его концом; все более длинные начала оказались
       неподходящими}
       while (x[len+1]<>х[i+1]) and (len>0) do begin
           {начало не подходит, применяем к нему функцию l}
         len:=l[len];
         end;
           {нашли подходящее или убедились в отсутствии}
         if x[len+1]=x[i+1] do begin
           {х[1]..x[len] - самое длинное подходящее начало}
         l[i+1]:=len+1;
         end else begin
           {подходящих нет}
         l[i+1]:= 0;
       end;
       i:=i+1;
    end;
       Доказать, что число действий в приведенном только что алгоритме не
превосходит Cn для некоторой константы C.
Решение. Это не вполне очевидно: обработка каждой очередной буквы может
потребовать многих итераций во внутреннем цикле. Однако каждая такая
итерация уменьшает len по крайней мере на 1, и в этом случае l[i+1]
окажется заметно меньше l[i]. С другой стороны, при увеличении i на единицу
величина l[i] может возрасти не более чем на 1, так что часто и сильно
убывать она не может - иначе убывание не будет скомпенсировано
возрастанием.
     Более точно, можно записать неравенство
        l[i+1]<l [i] - (число итераций на i-м шаге)+1
или
       (число итераций на i-м шаге)<= l[i]-l[i+1]+1
Остается сложить эти неравенства по всем i и получить оценку
сверху для общего числа итераций.
       Будем использовать этот алгоритм, чтобы выяснить, является ли слово
X длины n подсловом слова Y длины m. (Как это делать с помощью специального
разделителя #, описано выше.) При этом число действий будет не более
C(n+m}, и используемая память тоже. Придумать, как обойтись памятью не
более Cn (что может быть существенно меньше, если искомый образец короткий,
а слово, в котором его ищут - длинное).
Решение. Применяем алгоритм КМП к слову А#В. При этом: вычисление значений
l[1],...,l [n] проводим для слова X длины n и запоминаем эти значения.
Дальше мы помним только значение l[i] для текущего i - кроме него и кроме
таблицы
l[1]...l[n], нам для вычислений ничего не нужно.
     На практике слова X и Y могут не находиться подряд, поэтому просмотр
слова X и затем слова Y удобно оформить в виде разных циклов. Это избавляет
также от хлопот с разделителем.
     Написать соответствующий алгоритм (проверяющий, является ли слово
X=x[1]...x[n] подсловом слова Y=y[1]...y[m]
Решение. Сначала вычисляем таблицу l[1]...l[n]как раньше. Затем пишем такую
программу:

    j:=0; len:=0;
        {len - длина максимального качала слова X, одновременно
                   являющегося концом слова y[1]..j[j]}
    while (len<>n) and (j<>m) do begin
         while (x[len+1]<>у[j+1]) and (len>0) do begin
                {начало не подходит, применяем к нему функцию l}
             len: = l[len];
             end;
                {нашли подходящее или убедились в отсутствии}
             if x[len+1]=y[j+1] do begin
                {x[1]..x[len] - самое длинное подходящее начало}
             len:=len+1;
             end else begin
                {подходящих нет}
             len:=0;
         end;
         j:=j+1;
    end;
    {если len=n, слово X встретилось; иначе мы дошли до конца
    слова Y, так и не встретив X}

Алгоритм Бойера - Мура
   Этот алгоритм делает то, что на первый взгляд кажется невозможным: в
типичной ситуации он читает лишь небольшую часть всех букв слова, в котором
ищется заданный образец. Как так может быть? Идея проста. Пусть, например,
мы ищем образец abcd. Посмотрим на четвертую букву слова: если, к примеру,
это буква e, то нет никакой необходимости читать первые три буквы. (В самом
деле, в образце буквы e нет, поэтому он может начаться не раньше пятой
буквы.)
   Мы приведем самый простой вариант этого алгоритма, который не
гарантирует быстрой работы во всех случаях. Пусть x[1]...х[n] - образец,
который надо искать. Для каждого символа s найдем самое правое его
вхождение в слово X, то есть наибольшее k, при котором х[k]=s. Эти сведения
будем хранить в массиве pos[s]; если символ s вовсе не встречается, то нам
будет удобно положить pos[s]=0 (мы увидим дальше, почему).

  Как заполнить массив pos?
Решение.
положить все pos[s] равными 0
         for i:=1 to n do begin
             pos[x[i]]:=i;
         end;
В процессе поиска мы будем хранить в переменной last номер буквы в слове,
против которой стоит последняя буква образца. Вначале last=n (длина
образца), затем last постепенно увеличивается.
         last:=n;
         {все предыдущие положения образца уже проверены}
         while last<= m do begin {слово не кончилось}
               if x[m]<>y[last] then begin {последние буквы разные}
                  last:=last+(n-pos[y[last]]);
                 {n - pos[y[last]] - это минимальный сдвиг образца,
                        при котором напротив y[last] встанет такая же
                        буква в образце. Если такой буквы нет вообще,
                        то сдвигаем на всю длину образца}
               end else begin
              если нынешнее положение подходит, т.е. если
                     x[i]..х[n]=y[last-n+1]..y[last],
                     то сообщить о совпадении;
                     last:=last+1;
               end;
         end;
Знатоки рекомендуют проверку совпадения проводить справа налево, т.е.
начиная с последней буквы образца (в которой совпадение заведомо есть).
Можно также немного сэкономить, произведя вычитание заранее и храня не
pos[s], а n-pos[s],
т.е. число букв в образце справа от последнего вхождения буквы Возможны
разные модификации этого алгоритма. Например, можно строку
         last:=last+i
заменить на
         last:=last+(n-u),
где u - координата второго справа вхождения буквы x[n] в образец.

Как проще всего учесть это в программе
Решение. При построении таблицы pos написать
         for i:=1 to n-1 do...
(далее как раньше), а в основной программе вместо
         last:=last+1
написать
         last:=last+n-pos[y[last]];
Приведенный упрощенный вариант алгоритма Бойера-Мура в некоторых случаях
требует существенно больше n действий (число действий порядка mn),
проигрывая алгоритму Кнута-Морриса-Пратта.
 Пример ситуации, в которой образец не входит в слово, но алгоритму
требуется порядка mn действий, чтобы это установить.
Решение. Пусть образец имеет вид baaa... aa, а само слово состоит только из
букв а. Тогда на каждом шаге несоответствие выясняется лишь в последний
момент.
Настоящий (не упрощенный) алгоритм Бойера-Мура гарантирует, что число
действий не превосходит C(m+n) в худшем случае. Он использует идеи, близкие
к идеям алгоритма Кнута-Морриса-Пратта. Представим себе, что мы сравнивали
образец со входным словом, идя справа налево. При этом некоторый кусок Z
(являющийся концом образца) совпал, а затем обнаружилось различие: перед Z
в образце стоит не то, что во входном слове. Что можно сказать в этот
момент о
входном слове? В нем обнаружен фрагмент, равный Z, а перед ним стоит не та
буква, что в образце. Эта информация может позволить сдвинуть образец на
несколько позиций вправо без риска пропустить его вхождение. Эти сдвиги
следует вычислить заранее для каждого конца Z нашего образца. Как говорят
знатоки, все это (вычисление таблицы сдвигов и ее использование) можно
уложить в C(m+ n) действий.

Алгоритм Рабина

    Этот алгоритм основан на простой идее. Представим себе, что в слове
длины m мы ищем образец длины n. Вырежем окошечко размера n и будем двигать
его по входному слову. Нас интересует, не совпадает ли слово в окошечке с
заданным
образцом. Сравнивать по буквам долго. Вместо этого фиксируем некоторую
функцию, определенную на словах длины n. Если значения этой функции на
слове в окошечке и на образце различны, то совпадения нет. Только если
значения одинаковы, нужно проверять совпадение по буквам.
      В чем выигрыш при таком подходе. Казалось бы, ничего - ведь чтобы
вычислить значение функции на слове в окошечке, все равно нужно прочесть
все буквы этого слова. Так уж лучше их сразу сравнить с образцом. Тем не
менее выигрыш возможен, и вот за счет чего. При сдвиге окошечка слово не
меняется полностью, а лишь добавляется буква в конце и убирается в начале.
Хорошо бы, чтобы по этим данным можно было рассчитать, как меняется
функция.
   Привести пример удобной для вычисления функции.
Решение. Заменим все буквы в слове и образце их номерами, представляющими
собой целые числа. Тогда удобной функцией является сумма цифр. (При сдвиге
окошечка нужно добавить новое число и вычесть пропавшее.)
     Для каждой функции существуют слова, к которым она применима плохо.
Зато другая функция в этом случае может работать хорошо. Возникает идея:
надо запасти много функций и в начале работы алгоритма выбирать из них
случайную. (Тогда враг, желающий подгадить нашему алгоритму, не будет
знать, с какой именно функцией ему бороться.)
 Привести пример семейства удобных функций.
Решение. Выберем некоторое число p (желательно простое, смотри далее) и
некоторый вычет x по модулю p. Каждое слово длины n будем рассматривать как
последовательность целых чисел (заменив буквы кодами). Эти числа будем
рассматривать как коэффициенты многочлена степени n-1 и вычислим значение
этого многочлена по модулю p в точке x. Это и будет одна из функций
семейства (для каждой пары p и x получается, таким образом, своя функция).
Сдвиг окошка на 1 соответствует вычитанию старшего члена (хn-1 следует
вычислить заранее), умножению на x и добавлению свободного члена.
       Следующее соображение говорит в пользу того, что совпадения не
слишком вероятны. Пусть число p фиксировано и к тому же простое, а X и Y -
два различных слова длины n. Тогда им соответствуют различные многочлены
(мы предполагаем, что коды всех букв различны - это возможно, если p больше
числа букв алфавита). Совпадение значений функции означает, что в точке x
эти два различных многочлена совпадают, то есть их разность обращается в 0.
Разность есть многочлен степени n-1 и имеет не более n-1 корней. Таким
образом, если и много меньше p, то случайному x мало шансов попасть в
неудачную точку.