Россия, Пошатово |
Сопоставление с образцом
10.3. Вспомогательные утверждения
Для произвольного слова рассмотрим все его начала, одновременно являющиеся его концами, и выберем из них самое длинное. (Не считая, конечно, самого слова .) Будем обозначать его .
Примеры: , , ,
10.3.1. Доказать, что все слова , , и т.д. являются началами слова .
Решение. Каждое из них (согласно определению) является началом предыдущего.
По той же причине все они являются концами слова .
10.3.2. Доказать, что последовательность предыдущей задачи обрывается (на пустом слове).
Решение. Каждое слово короче предыдущего.
10.3.3. Доказать, что любое слово, одновременно являющееся началом и концом слова (кроме самого ) входит в последовательность
Решение. Пусть слово есть одновременно начало и конец . Слово - самое длинное из таких слов, так что не длиннее . Оба эти слова являются началами , поэтому более короткое из них является началом более длинного: есть начало . Аналогично, есть конец . Рассуждая по индукции, можно предполагать, что утверждение задачи верно для всех слов короче , в частности, для слова . Так что слово , являющееся концом и началом , либо равно , либо входит в последовательность , что и требовалось доказать.
10.4. Алгоритм Кнута-Морриса-Пратта
Алгоритм Кнута-Морриса-Пратта (КМП) получает на вход слово
и просматривает его слева направо буква за буквой, заполняя при этом массив натуральных чисел , где (функция определена в предыдущем пункте). Словами: l[i] есть длина наибольшего начала слова , одновременно являющегося его концом.10.4.1. Какое отношение все это имеет к поиску подслова? Другими словами, как использовать алгоритм КМП для определения того, является ли слово A подсловом слова B?
Решение. Применим алгоритм КМП к слову A\#B, где \# - специальная буква, не встречающаяся ни в A, ни в B. Слово A является подсловом слова B тогда и только тогда, когда среди чисел в массиве l будет число, равное длине слова A.
10.4.2. Описать алгоритм заполнения таблицы .
Решение. Предположим, что первые i значений уже найдены. Мы читаем очередную букву слова (т.е. x[i+1] ) и должны вычислить l[i+1].
Другими словами, нас интересуют начала слова , одновременно являющиеся его концами - из них нам надо выбрать самое длинное. Откуда берутся эти начала? Каждое из них (не считая пустого) получается из некоторого слова приписыванием буквы x[i+1]. Слово является началом и концом слова . Однако не любое слово, являющееся началом и концом слова , годится - надо, чтобы за ним следовала буква x[i+1].
Получаем такой рецепт отыскания слова . Рассмотрим все начала слова , являющиеся одновременно его концами. Из них выберем подходящие - те, за которыми идет буква . Из подходящих выберем самое длинное. Приписав в его конец x[i+1], получим искомое слово .
Теперь пора воспользоваться сделанными нами приготовлениями и вспомнить, что все слова, являющиеся одновременно началами и концами данного слова, можно получить повторными применениями к нему функции из предыдущего раздела. Вот что получается:
i:=1; l[1]:= 0; {таблица l[1]..l[i] заполнена правильно} while i <> n do begin | len := l[i] | {len - длина начала слова x[1]..x[i], которое является | его концом; все более длинные начала оказались | неподходящими} | while (x[len+1] <> x[i+1]) and (len > 0) do begin | | {начало не подходит, применяем к нему функцию l} | | len := l[len]; | end; | {нашли подходящее или убедились в отсутствии} | if x[len+1] = x[i+1] do begin | | {x[1]..x[len] - самое длинное подходящее начало} | | l[i+1] := len+1; | end else begin | | {подходящих нет} | | l[i+1] := 0; | end; | i := i+1; end;
10.4.3. Доказать, что число действий в приведенном только что алгоритме не превосходит для некоторой константы .
Решение. Это не вполне очевидно: обработка каждой очередной буквы может потребовать многих итераций во внутреннем цикле. Однако каждая такая итерация уменьшает len по крайней мере на 1, и в этом случае l[i+1] окажется заметно меньше l[i]. С другой стороны, при увеличении i на единицу величина l[i] может возрасти не более чем на 1, так что часто и сильно убывать она не может - иначе убывание не будет скомпенсировано возрастанием.
Более точно, можно записать неравенство
или Остается сложить эти неравенства по всем i и получить оценку сверху для общего числа итераций.10.4.4. Будем использовать этот алгоритм, чтобы выяснить, является ли слово X длины n подсловом слова Y длины m. (Как это делать с помощью специального разделителя \#, описано выше.) При этом число действий будет не более , и используемая память тоже. Придумать, как обойтись памятью не более (что может быть существенно меньше, если искомый образец короткий, а слово, в котором его ищут - длинное).
Решение. Применяем алгоритм КМП к слову . При этом вычисление значений проводим для слова X длины n и запоминаем эти значения. Дальше мы помним только значение l[i] для текущего i - кроме него и кроме таблицы , нам для вычислений ничего не нужно.
На практике слова X и Y могут не находиться подряд, поэтому просмотр слова X и затем слова Y удобно оформить в виде разных циклов. Это избавляет также от хлопот с разделителем.
10.4.5. Написать соответствующий алгоритм (проверяющий, является ли слово подсловом слова ).
Решение. Сначала вычисляем таблицу как раньше. Затем пишем такую программу:
j:=0; len:=0; {len - длина максимального начала слова X, одновременно являющегося концом слова y[1]..y[j]} while (len <> n) and (j <> m) do begin | while (x[len+1] <> y[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}