10.1.1.
Имеется последовательность символов $${x[1]}\ldots{x[n]}$$. Определить, имеются ли в ней
идущие друг за другом символы . (Другими словами,
требуется выяснить, есть ли в слове $${x[1]}\ldots{x[n]}$$ подслово .)
Решение. Имеется примерно n (если быть точным, n-3 ) позиций, на которых может находиться искомое
подслово в исходном слове. Для каждой из позиций можно
проверить, действительно ли там оно находится, сравнив
четыре символа. Однако есть более эффективный способ. Читая
слово $${x[1]}\ldots{x[n]}$$ слева направо, мы ожидаем
появления буквы a. Как только она появилась, мы ищем за
ней букву b, затем c, и, наконец, d. Если наши
ожидания оправдываются, то слово обнаружено. Если
же какая-то из нужных букв не появляется, мы оказываемся
у разбитого корыта и начинаем все сначала.
Этот простой алгоритм можно описать в разных терминах.
Используя терминологию так называемых конечных
x слева направо мы
в каждый момент находимся в одном из следующих состояний:
"начальное" (0), "сразу после a " (1),
"сразу после ab " (2), "сразу после " (3) и "сразу после " (4).
Читая очередную букву, мы переходим в следующее состояние
по правилу, указанному в таблице.
| Текущее состояние | Очередная буква | Новое состояние |
|---|---|---|
0 |
a |
1 |
0 |
кроме а |
0 |
1 |
b |
2 |
1 |
a |
1 |
1 |
кроме a, b |
0 |
2 |
c |
3 |
2 |
a |
1 |
2 |
кроме a, c |
0 |
3 |
d |
4 |
3 |
a |
1 |
3 |
кроме a, d |
0 |
Как только мы попадем в состояние 4, работа заканчивается.
Наглядно выполнение алгоритма можно представить себе так: фишка двигается из кружка в кружок по стрелкам; стрелка выбирается так, чтобы надпись на ней соответствовала очередной букве входного слова. Чтобы этот процесс был успешным, нужно, чтобы для каждой буквы была ровно одна подходящая стрелка из любого кружка.
Соответствующая программа очевидна (мы указываем новое состояние, даже если оно совпадает со старым; эти строки можно опустить):
i:=1; state:=0;
{i - первая непрочитанная буква, state - состояние}
while (i <> n+1) and (state <> 4) do begin
| if state = 0 then begin
| | if x[i] = a then begin
| | | state:= 1;
| | end else begin
| | | state:= 0;
| | end;
| end else if state = 1 then begin
| | if x[i] = b then begin
| | | state:= 2;
| | end else if x[i] = a then begin
| | | state:= 1;
| | end else begin
| | | state:= 0;
| | end;
| end else if state = 2 then begin
| | if x[i] = c then begin
| | | state:= 3;
| | end else if x[i] = a then begin
| | | state:= 1;
| | end else begin
| | | state:= 0;
| | end;
| end else if state = 3 then begin
| | if x[i] = d then begin
| | | state:= 4;
| | end else if x[i] = a then begin
| | | state:= 1;
| | end else begin
| | | state:= 0;
| | end;
| end;
end;
answer := (state = 4);
Иными словами, мы в каждый момент храним информацию о том,
какое максимальное начало нашего образца является
концом прочитанной части. (Его длина и есть то "
состояние", о котором шла речь.)
Терминология, нами используемая, такова. Слово - это любая последовательность символов из некоторого фиксированного конечного множества. Это множество называется алфавитом, его элементы - буквами. Если отбросить несколько букв с конца слова, останется другое слово, называемое началом первого. Любое слово также считается своим началом. Конец слова - то, что останется, если отбросить несколько первых букв. Любое слово считается своим концом. Подслово - то, что останется, если отбросить буквы и с начала, и с конца. (Другими словами, подслова - это концы начал, или, что то же, начала концов.)
В терминах
своим подсловом. Эта функция не является индуктивной, но
имеет
10.2.1.
Можно ли в предыдущих рассуждениях заменить слово на произвольное слово?
Решение. Нет, и проблемы связаны с тем, что в образце
могут быть повторяющиеся буквы. Пусть, например, мы ищем
вхождения слова ababc. Вот появилась буква a, за
ней идет b, за ней идет a, затем снова b.
В этот момент мы с нетерпением ждем буквы c. Однако -
к нашему разочарованию - вместо нее появляется другая
буква, и наш образец ababc не обнаружен. Однако нас
может ожидать утешительный приз: если вместо c
появилась буква a, то не все потеряно: за ней могут
последовать буквы b и c, и образец-таки будет
найден.
Вот картинка, поясняющая сказанное:$${\setlength{\tabcolsep}{.8\tabcolsep}
\begin{tabular}{llllllllllllll}
x y z a b a b a b c
\ldots\leftarrowвходное слово\\
a b a b c
\leftarrowмы ждали
образца здесь\\
a b a b c
\leftarrowа он
оказался здесь\\
\end{tabular}$$
Таким образом, к моменту$${\setlength{\tabcolsep}{.8\tabcolsep}
\begin{tabular}{lllllll|lllllll}
x y z a b a b
\leftarrowвходное слово\\
a b a b c
\leftarrowмы ждали
образца здесь\\
a b a b c
\leftarrowа он
оказался здесь\\
\end{tabular}$$
есть два возможных положения образца, каждое из которых
подлежит проверке. Тем не менее по-прежнему возможен
конечный
10.2.2.
Указать состояния соответствующего
Решение. По-прежнему состояния будут соответствовать
наибольшему началу образца, являющемуся концом прочитанной
части слова. Их будет шесть: 0, 1 (a), 2 (ab),
3 (aba), 4 (abab), 5 (ababc). Таблица перехода
такая.
| Текущее состояние | Очередная буква | Новое состояние |
|---|---|---|
0 |
a |
1 (a) |
0 |
кроме а |
0 |
1 (a) |
b |
2 (ab) |
1 (a) |
a |
1 (a) |
1 (a) |
кроме a, b |
0 |
2 (ab) |
a |
3 (aba) |
2 (ab) |
кроме a |
0 |
3 (aba) |
b |
4 (abab) |
3 (aba) |
a |
1 (a) |
3 (aba) |
кроме a, b |
0 |
4 (abab) |
c |
5 (ababc) |
4 (abab) |
a |
3 (aba) |
4 (abab) |
кроме a, c |
0 |
Для проверки посмотрим, к примеру, на вторую снизу строку.
Если прочитанная часть кончалась на abab, а затем
появилась буква a, то теперь прочитанная часть
кончается на ababa. Наибольшее начало образца
( ababc ), являющееся ее концом - это aba.
Философский вопрос: мы говорили, что трудность
состоит в том, что есть несколько возможных положений
образца, каждое из которых может оказаться
Философский ответ. Дело в том, что самое длинное из них определяет все остальные - это его концы, одновременно являющиеся его началами.
Не составляет труда для любого конкретного образца написать
программу, осуществляющую поиск этого образца описанным
способом. Однако хотелось бы написать программу, которая
ищет произвольный образец в произвольном слове. Это можно
делать в два этапа: сначала по образцу строится таблица
переходов конечного
Для произвольного слова $$X$$ рассмотрим все его начала, одновременно являющиеся его концами, и выберем из них самое длинное. (Не считая, конечно, самого слова $$X$$.) Будем обозначать его $$l(X)$$.
Примеры: $$l({aba})={a}$$, $$l({abab})={ab}$$, $$l({ababa})={aba}$$, $$l({abc}) = \text{пустое слово}$$
10.3.1. Доказать, что все слова $$l(X)$$, $$l(l(X))$$, $$l(l(l(X)))$$ и т.д. являются началами слова $$X$$.
Решение. Каждое из них (согласно определению) является началом предыдущего.
По той же причине все они являются концами слова $$X$$.
10.3.2. Доказать, что последовательность предыдущей задачи обрывается (на пустом слове).
Решение. Каждое слово короче предыдущего.
10.3.3. Доказать, что любое слово, одновременно являющееся началом и концом слова $$X$$ (кроме самого $$X$$ ) входит в последовательность $$l(X), l(l(X)),\ldots$$
Решение. Пусть слово $$Y$$ есть одновременно начало
и конец $$X$$. Слово $$l(X)$$ - самое длинное из таких слов,
так что $$Y$$ не длиннее $$l(X)$$. Оба эти слова являются
началами $$X$$, поэтому более короткое из них является
началом более длинного: $$Y$$ есть начало $$l(X)$$.
Аналогично, $$Y$$ есть конец $$l(X)$$. Рассуждая по
l[i] есть длина наибольшего начала слова $${x[1]}\ldots{x[i]}$$, одновременно являющегося его
концом.
10.4.1.
Какое отношение все это имеет к поиску подслова? Другими
словами, как использовать алгоритм КМП для определения
того, является ли слово A подсловом слова B?
Решение. Применим алгоритм КМП к слову A\#B, где \# - специальная буква, не встречающаяся ни в A, ни
в B. Слово A является подсловом слова B тогда
и только тогда, когда среди чисел в массиве l будет
число, равное длине слова A.
10.4.2. Описать алгоритм заполнения таблицы $${l[1]}\ldots{l[n]}$$.
Решение. Предположим, что первые i значений $${l[1]}\ldots{l[i]}$$ уже найдены. Мы читаем очередную
букву слова (т.е. x[i+1] ) и должны вычислить l[i+1].
Другими словами, нас интересуют начала $$Z$$ слова $${x[1]}\ldots{x[i+1]}$$, одновременно являющиеся его
концами - из них нам надо выбрать самое длинное. Откуда
берутся эти начала? Каждое из них (не считая пустого)
получается из некоторого слова $$Z'$$ приписыванием буквы x[i+1]. Слово $$Z'$$ является началом и концом слова $${x[1]}\ldots{x[i]}$$. Однако не любое слово, являющееся
началом и концом слова $${x[1]}\ldots{x[i]}$$, годится -
надо, чтобы за ним следовала буква x[i+1].
Получаем такой рецепт отыскания слова $$Z$$. Рассмотрим все
начала слова $${x[1]}\ldots{x[i]}$$, являющиеся
одновременно его концами. Из них выберем подходящие - те,
за которыми идет буква $${x[i+1]}$$. Из подходящих выберем
самое длинное. Приписав в его конец x[i+1], получим
искомое слово $$Z$$.
Теперь пора воспользоваться сделанными нами приготовлениями и вспомнить, что все слова, являющиеся одновременно началами и концами данного слова, можно получить повторными применениями к нему функции $$l$$ из предыдущего раздела. Вот что получается:
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.
Доказать, что число действий в приведенном только что
алгоритме не превосходит $$C{n}$$ для некоторой
Решение. Это не вполне очевидно: обработка каждой
очередной буквы может потребовать многих по крайней мере на 1, и в этом случае l[i+1] окажется заметно меньше l[i]. С другой
стороны, при увеличении i на единицу величина l[i]
может возрасти не более чем на 1, так что часто
и сильно убывать она не может - иначе убывание не будет
скомпенсировано возрастанием.
Более точно, можно записать неравенство$${l[i+1]} \le {l[i]} - \hbox{(число итераций на {i}-м шаге)} + {1}$$
или$$\hbox{(число итераций на {i}-м шаге)}\le{l[i]}-{l[i+1]} + {1}.$$
Остается сложить эти i и получить
оценку сверху для общего числа
10.4.4.
Будем использовать этот алгоритм, чтобы выяснить, является
ли слово X длины n подсловом слова Y
длины m. (Как это делать с помощью специального
разделителя \#, описано выше.) При этом число действий
будет не более $$C({n}+{m})$$, и используемая память
тоже. Придумать, как обойтись памятью не более $$C{n}$$
(что может быть существенно меньше, если искомый образец
короткий, а слово, в котором его ищут - длинное).
Решение. Применяем алгоритм КМП к слову $${A#B}$$. При
этом X длины n и запоминаем эти
значения. Дальше мы помним только значение l[i] для
текущего i - кроме него и кроме таблицы $${l[1]}\ldots{l[n]}$$, нам для вычислений ничего не
нужно.
На практике слова X и Y могут не находиться подряд,
поэтому просмотр слова X и затем слова Y удобно
оформить в виде разных циклов. Это избавляет также от
хлопот с разделителем.
10.4.5. Написать соответствующий алгоритм (проверяющий, является ли слово $${X}={x[1]}\ldots{x[n]}$$ подсловом слова $${Y}={y[1]}\ldots{y[m]}$$ ).
Решение. Сначала вычисляем таблицу $${l[1]}\ldots{l[n]}$$ как раньше. Затем пишем такую программу:
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}
Этот алгоритм делает то, что на первый взгляд кажется невозможным:
в типичной ситуации он читает лишь небольшую часть всех
букв слова, в котором ищется заданный образец. Как так
может быть? Идея проста. Пусть, например, мы ищем образец . Посмотрим на четвертую букву слова: если,
к примеру, это буква e, то нет никакой необходимости
читать первые три буквы. (В самом деле, в образце
буквы e нет, поэтому он может начаться не раньше пятой
буквы.)
Мы приведем самый простой вариант этого алгоритма, который
не гарантирует быстрой работы во всех случаях. Пусть $${x[1]}\ldots{x[n]}$$ - образец, который надо искать.
Для каждого символа s найдем самое правое его вхождение
в слово X, то есть наибольшее k, при котором $${x[k]}={s}$$. Эти сведения будем хранить в массиве ; если символ s вовсе не встречается, то нам
будет удобно положить $${pos[s]}={0}$$ (мы увидим дальше,
почему).
10.5.1.
Как заполнить ?
Решение.
положить все 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[n] <> y[last] then begin {последние буквы разные}
| | last := last + (n - pos[y[last]]);
| | {n - pos[y[last]] - это минимальный сдвиг образца,
| | при котором напротив y[last] встанет такая же
| | буква в образце. Если такой буквы нет вообще,
| | то сдвигаем на всю длину образца}
| end else begin
| | если нынешнее положение подходит, т.е. если
| | x[1]..x[n] = y[last-n+1]..y[last],
| | то сообщить о совпадении;
| | last := last+1;
| end;
end;
Знатоки рекомендуют проверку совпадения проводить справа
налево, т.е. начиная с последней буквы образца (в которой
совпадение заведомо есть). Можно также немного сэкономить,
произведя ,
а n-, т.е. число букв в образце справа от
последнего вхождения буквы s.
Возможны разные модификации этого алгоритма. Например,
можно строку last:=last+1 заменить на last:=last+(n-u), где u - x[n] в образец.
10.5.2. Как проще всего учесть это в программе?
Решение. При построении таблицы написать
for i:=1 to n-1 do...
(далее как раньше), а в last:=last+1 написать
last:= last+n-pos[y[last]];
Приведенный нами упрощенный вариант алгоритма
Бойера-Мура в некоторых случаях требует существенно
больше n действий (число действий порядка mn ),
проигрывая
10.5.3.
Привести пример ситуации, в которой образец не входит
в слово, но алгоритму требуется порядка mn действий,
чтобы это установить.
Решение. Пусть образец имеет вид $${baaa}\ldots{aa}$$,
а само слово состоит только из букв a. Тогда на каждом
шаге несоответствие выясняется лишь в последний момент.
Настоящий (не упрощенный) алгоритм Бойера-Мура
гарантирует, что число действий не превосходит $$C({m}+{n})$$ в худшем случае. Он использует идеи,
близкие к идеям
Этот алгоритм основан на простой идее. Представим себе, что в слове длины $$m$$ мы ищем образец длины $$n$$. Вырежем окошечко размера $$n$$ и будем двигать его по входному слову. Нас интересует, не совпадает ли слово в окошечке с заданным образцом. Сравнивать по буквам долго. Вместо этого фиксируем некоторую функцию, определенную на словах длины $$n$$. Если значения этой функции на слове в окошечке и на образце различны, то совпадения нет. Только если значения одинаковы, нужно проверять совпадение по буквам.
Что мы выигрываем при таком подходе? Казалось бы, ничего -
ведь чтобы вычислить
10.6.1. Привести пример удобной для вычисления функции.
Решение. Заменим все буквы в слове и образце их номерами,
представляющими собой
Для каждой функции существуют слова, к которым она применима плохо. Зато другая функция в этом случае может работать хорошо. Возникает идея: надо запасти много функций и в начале работы алгоритма выбирать из них случайную. (Тогда враг, желающий подгадить нашему алгоритму, не будет знать, с какой именно функцией ему бороться.)
10.6.2. Привести пример семейства удобных функций.
Решение. Выберем некоторое число $$p$$ (желательно простое,
смотри далее) и некоторый
Следующее соображение говорит в пользу того, что совпадения
не слишком
Мы можем искать не конкретное слово, а подслова заданного
вида. Например, можно искать слова вида a?b, где
вместо ? может стоять любая буква (иными словами, нас
интересует буква b на a ).
10.7.1.
Указать конечный a?b.
Решение. Читая слово, следует помнить, есть ли буква a
на последнем месте и на предпоследнем - пока не встретим
искомый фрагмент. 00, 01, 10, 11,
их смысл таков:$$\begin{tabular}{rl}
00 на предпоследнем и последнем местах нет {a}\\
01 на предпоследнем нет, на последнем есть\\
10 не предпоследнем есть, на последнем нет\\
11 есть и там, и там
\end{tabular}$$
Таблица переходов
Другой стандартный знак в образце - это звездочка ( * ),
на место которой может быть подставлено любое слово.
Например, образец ab* означает, что мы ищем подслово ab, за которым следует что угодно, а затем (на любом
.
10.7.2.
Указать конечный ab* (в описанном только что смысле).
Решение.$$\raisebox{\depth}{\begin{tabular}{|c|c|c|} \hline Текущее Очередная Новое \\ состояние буква состояние \\ \hline начальное {a} {a} \\ начальное не {a} начальное \\ a {b} {ab} \\ a {a} {a} \\ a не {a} и не {b} начальное \\ {ab} {c} {abc} \\ {ab} не {c} {ab} \\ {abc} {d} найдено \\ {abc} {c} {abc} \\ {abc} не {c} и не {d} {ab} \\ \hline \end{tabular}}$$
Еще один вид поиска - это поиск любого из слов некоторого списка.
10.7.3.
Дан список слов $$X_1,\ldots,X_k$$ и слово $$Y$$. Определить,
входит ли хотя бы одно из слов $$X_i$$ в слово $$Y$$ (как
подслово). Количество действий не должно превосходить
Решение. Очевидный способ состоит в том, чтобы каждое слово из списка проверять отдельно (с помощью одного из рассмотренных алгоритмов). Однако при этом мы не укладываемся в заданное число действий (из-за умножения $$k$$ на длину слова $$Y$$ ).
Посмотрим на дело с другой стороны. Каждому образцу из
списка соответствует конечный
Вспомним
Склеим все образцы в
Формально говоря, вершинами
Читая входное слово, мы двигаемся по этому дереву: текущая вершина - это наибольшая (самая правая) из вершин, являющихся концом прочитанной части ( $${}={}$$ наибольший конец прочитанной части, являющийся началом одного из образцов).
Определим функцию $$l$$, аргументами и значениями которой
являются вершины
10.7.4.
Пусть $$P$$ - вершина
Решение. См.
Теперь ясно, что нужно делать, находясь в вершине $$P$$
и читая букву z входного слова. Надо просматривать
последовательно вершины $$P$$, $$l(P)$$, $$l(l(P)),..$$., пока
не обнаружится такая, из которой выходит стрелка
с буквой z. Та вершина, в которую эта стрелка ведет,
и будет нашим следующим положением.
Остается понять, как для каждой вершины
Можно поинтересоваться, какие свойства слов распознаются
с помощью конечных
Пусть фиксирован конечный (, ), * и | (они будут использоваться для
построения
Г - A,B,C,...,E - (ABC ...E) - A,B,C,...,E - (A|B|C|...|E) - A - A* - Каждое
(ABC ...E)
соответствует множество всех слов, которые можно
получить, если к слову из A приписать слово
из B, затем из C,..., затем
из E(A|B|C|...|E) соответствует
A,B,C,...,E ;A{*} соответствует
итерация множества, соответствующего
выражению A, то есть множество всех слов,
которые можно так разрезать на куски, что каждый
кусок принадлежит множеству, соответствующему
выражению A. (В частности, пустое слово всегда
содержится в A*.)Множества, соответствующие регулярным выражениям,
Называются
10.7.5.
Написать a и b, в которых число
букв a четно.
Решение. Выражение b* задает все слова без
буквы a, а выражение $${(b*}\,{a}\,{b*}\,{a}\,{b*)}$$ - все слова ровно
с двумя буквами a. Остается объединить эти множества,
а потом применить
10.7.6.
Написать bac является подсловом.
Решение. $${((a|b|c)*}\,{bac}\,{(a|b|c)*)}$$
10.7.7.
Написать bac не является подсловом.
Указание.
Эта задача сложнее предыдущей; видимо, самый простой способ
ее решить - перейти к конечным
Теперь задачу о поиске образца в слове можно
переформулировать так: проверить, принадлежит ли слово
множеству, соответствующему данному
10.7.8.
Какие выражения соответствуют образцам a?b и ab*,
рассмотренным ранее? (В образце символ * используется
не в том смысле, что в регулярных выражениях!)
Предполагается, что
Решение.
((a|b|c|d|e)*a(a|b|c|d|e)b(a|b|c|d|e)*) ((a|b|c|d|e)*ab(a|b|c|d|e)*cd(a|b|c|d|e)*)
10.7.9.
Доказать, что для всякого
Решение. Нам потребуется новое понятие - понятие
Будем двигаться различными способами из Н в К, читая буквы по дороге (на тех стрелках, где они есть). Каждому пути из Н в К, таким образом, соответствует некоторое слово. А источнику в целом соответствует множество слов - тех слов, которые можно прочесть на путях из Н в К.
Замечание. Если нарисовать состояния конечного
Мы будем строить
10.7.10.
По
Решение.
Нарисована картинка для
Наконец,
10.7.11.
Дан источник. Построить конечный
Решение. Состояниями
Тем самым задача решена.
Оказывается, что
10.7.12.
Дан источник. Построить
Решение. Пусть источник имеет вершины $$1,\ldots,k$$. Будем считать, что $$1$$ - это начало, а $$k$$ - конец. Через $$D_{i,j,s}$$ обозначим множество всех слов, которые можно прочесть на пути из $$i$$ в $$j$$, если в качестве промежуточных пунктов разрешается использовать только вершины $$1,\ldots,s$$. Согласно определению, источнику соответствует множество $$D_{1,k,k}$$.
Из чего состоит множество $$D_{i,j,s+1}$$? Отметим на пути
моменты, в которых он заходит в $$(s+1)$$ -ую вершину. При
этом путь разбивается на части, каждая из которых уже не
заходит в нее. Поэтому легко сообразить, что$$D_{i,j,s+1} =
D_{i,j,s}\,|\, (D_{i,s+1,s}\;\;\; D_{s+1,s+1,s}{*}\;\;\; D_{s+1,j,s})$$
(вольность записи: мы используем для операций над
множествами обозначения как для
10.7.13. Где еще используется то же самое рассуждение?
Ответ. В
10.7.14.
Доказать, что класс множеств, задаваемых регулярными
выражениями, не изменился бы, если бы мы разрешили
использовать не только
Решение. Для
Замечание. На практике важную роль играет число состояний
До сих про наши программы сначала получали образец, который надо искать, а потом текст, в котором надо искать. В следующих задачах все наоборот.
10.8.1.
Программа получает на вход слово $$Y$$ длины $$m$$ и может
его
обрабатывать (пока без ограничений на время и память).
Затем она получает слово $$X$$ длины $$n$$ и должна сообщить,
является ли оно подсловом слова $$Y$$. При этом число
операций при обработке слова $$X$$ должно быть порядка $$n$$
(не превосходить $$cn$$, где
Решение. Пока не накладывается никаких ограничений на
время и память при обработке $$Y$$, это не представляет
труда. Именно, надо склеить все подслова слова $$Y$$
в
Пусть такое
Заметим, что аналогичная конструкция годится для любого
множества слов $$U$$, а не только для множества всех подслов
данного слова: после того как соответствующее
10.8.2. Решить предыдущую задачу с дополнительным ограничением: объем используемой памяти пропорционален длине слова $$Y$$.
Решение. Прежний способ не годится: число вершин
Вот что получится при сжатии нашего примера:
Будем считать (здесь и далее), что последняя буква
слова $$Y$$ больше в нем не встречается. (Этого всегда можно
достичь, дописав дополнительный фиктивный символ.) Тогда
листья сжатого
У каждой внутренней вершины (не листа) сжатого
Построенное нами сжатое
10.8.3.
Показать, что построение
Решение. Будем добавлять в
Если это произойдет посередине
Гораздо более сложной задачей является построение
Для начала опишем более подробно структуру
Мы рассматриваем
Каждой вершине $$v$$ такого
Помимо вершин
Пусть $$p$$ - произвольная
Заметим, что при этом может образоваться новая вершина
(если развилка оказалась внутри
Оказывается, что навигацию в
10.8.4.
Пусть для данной позиции $$p$$ и слова $$w$$ заранее
известно, что в
Решение. В самом деле, при
Подведем итоги. Рассмотренный способ хранения деревьев позволяет (для фиксированного слова $$Y$$ )
O (число ребер на пути)];O (число букв в w, не вошедших
в l(q) ];В квадратных скобках указано число действий при выполнении соответствующих операций.
Еще мы будем хранить в вершинах
Начнем с полного (не
Вот как выглядят эти переходы в нашем примере (отрезание первой буквы соответствует пунктирной стрелке):
Эти стрелки мы будем называть
Формально можно сказать так. Пусть $$w'$$ означает
слово $$w$$
без первой буквы ( $$w'$$ определено для любого непустого
слова $$w$$ ). Тогда
10.8.5.
Как связаны
Ответ. Они указывают на соседние вершины, и буква на
соединяющем их
10.8.6.
Доказать, что при переходе к
Решение. В самом деле, по нашему предположению последняя
буква слова больше в нем не встречается, поэтому из листа
ссылка ведет в лист. А если вершина (отличная от корня)
является точкой
Вот что получится для нашего примера:
Теперь мы уже готовы к изложению алгоритма МакКрейта.
10.8.7.
Показать, что
Решение.
В самом деле,
Надо понять, как поддерживать это при добавлении очередного
Какие тут возможны
Оба варианта предполагают, что отец $$u$$ листа $$\textit{last}$$ не совпадает с корнем. (Если совпадает, нам придется добавлять $$Y_{i+1}$$ от корня.) Пусть $$\textit{tail}$$ - пометка листа $$\textit{last}$$, а $$\textit{head}=s(u)$$ ; другими словами, слово $$\textit{head}$$ соответствует вершине $$u$$. Тогда$$Y_i = \textit{head}+\textit{tail}.$$ Отрезая первую букву, получаем$$Y_{i+1}=\textit{head}\,'+\textit{tail}.$$
Заметим, что $$\textit{head}\,'$$ заведомо не выходит за
пределы
Поэтому мы можем сначала проследить $$\textit{head}\,'$$ (найти позицию $$v$$, для которой $$s(v)=\textit{head}\,'$$ ), а потом уже добавить $$\textit{tail}$$, начиная с $$v$$.
Первый способ
Эта
Тогда
$$Y_{i}=s(v)+p,\quad Y_{i+1}=Y_{i}'=s(v)'+p= s(w)+p,$$и для добавления $$Y_{i+1}$$ в
Второй способ
Итак, мы можем описать действия, выполняемые при добавление
очередного
Второй способ
Пусть $$u$$ - отец листа $$\textit{last}$$,
соответствующего
последнему уже добавленному
Случай 1: $$u$$ есть
Случай 2: $$u$$ не есть
Случай 3: $$u$$ не есть
Остается еще понять, как поддерживать структуру суффиксных ссылок (адрес нового листа у нас получается при добавлении сам собой, так что с ним проблем нет) и оценить число действий при выполнении этих процедур последовательно для всех суффиксов.
Начнем с суффиксных ссылок. По правилам они должны быть
у всех
Все сказанное можно условно записать в виде такого
алгоритма добавления
{ дерево содержит суффиксы Y1,...,Yi
s(last)=Yi
имеются корректные суффиксные ссылки для всех
внутренних вершин, кроме отца листа last}
u := отец листа last;
tail := пометка листа last;
Yi=s(u)+tail
if u=корень дерева then begin
| {Yi+1=tail'}
| добавить tail', начиная с корня,
| полученный лист поместить в last
end else begin}
| v := отец вершины u;
| pretail := пометка вершины u;
| {Yi=s(v)+pretail}+tail}
| if v = корень дерева then begin
| | {Yi+1=pretail'+tail}
| | проследить pretail' из корня в z
| end else begin
| | w := суффиксная ссылка вершины v;
| | s(w)=s(v)', Yi+1=s(w)+pretail+tail}
| | проследить pretail из w в z
| end;
| {осталось добавить tail из z и ссылку из u в z}
| if позиция z является вершиной then begin
| | поместить в u ссылку на z;
| | добавить tail, начиная с z,
| | полученный лист поместить в last;
| end else begin
| добавить tail, начиная с z,
| полученный лист поместить в last;
| поместить в u ссылку на отца листа last;
| end
end;
Осталось оценить число действий, которые выполняются при
последовательном добавлении суффиксов $$Y_1,\ldots, Y_m$$.
При добавлении каждого следующего
Прослеживания. Длительность прослеживания
пропорциональна числу $$k$$ задействованных в нем ребер, но
при этом
Добавления. Рассуждаем аналогично, но следим не за высотой последнего листа, а за длиной его пометки. При добавлении слова $$\textit{tail}$$ (или $$\textit{tail}\,'$$ ) число действий пропорционально числу просмотренных букв, но каждая просмотренная буква (кроме, быть может, одной) уменьшает длину пометки хотя бы на единицу: в пометке остаются лишь непросмотренные буквы (не считая первой). Поэтому на все добавления уходит в общей сложности $$O(m)$$ действий.
Тем самым мы доказали, что описанный алгоритм строит сжатое
10.8.8.
Как модифицировать алгоритм построения
Решение. Каждая вершина
10.8.9. Как модифицировать этот алгоритм, чтобы для каждого подслова можно было бы указывать его первое (самое левое) вхождение?
Указание.
При возникновении новой вершины на
10.8.10. Как модифицировать этот алгоритм, чтобы для каждого подслова можно было бы указывать его последнее (самое правое) вхождение?
Указание.
Если при каждом проходе корректировать информацию вдоль
пути, это будет долго; быстрее построить
10.8.11.
Как использовать
Решение. Такое подслово является внутренней вершиной
На практике можно использовать также и другой способ
нахождения самого длинного подслова, входящего дважды, -
так называемый
Этот способ требует меньше памяти (нам нужен лишь один
10.8.12. Применить один из таких алгоритмов к любимой книге и объяснить результат.
Указание. Длинные повторяющиеся куски могут быть художественным приемом (как в известном стишке про дом, который построил Джек) или следствием забывчивости автора. Для современных авторов возможно также неумеренное использование функций вырезания и вставки (заливки текста в мышь и выливания из мыши, если использовать графический интерфейс) в текстовом редакторе.
10.1.1.
Имеется последовательность символов $${x[1]}\ldots{x[n]}$$. Определить, имеются ли в ней
идущие друг за другом символы . (Другими словами,
требуется выяснить, есть ли в слове $${x[1]}\ldots{x[n]}$$ подслово .)
Решение. Имеется примерно n (если быть точным, n-3 ) позиций, на которых может находиться искомое
подслово в исходном слове. Для каждой из позиций можно
проверить, действительно ли там оно находится, сравнив
четыре символа. Однако есть более эффективный способ. Читая
слово $${x[1]}\ldots{x[n]}$$ слева направо, мы ожидаем
появления буквы a. Как только она появилась, мы ищем за
ней букву b, затем c, и, наконец, d. Если наши
ожидания оправдываются, то слово обнаружено. Если
же какая-то из нужных букв не появляется, мы оказываемся
у разбитого корыта и начинаем все сначала.
Этот простой алгоритм можно описать в разных терминах.
Используя терминологию так называемых конечных
x слева направо мы
в каждый момент находимся в одном из следующих состояний:
"начальное" (0), "сразу после a " (1),
"сразу после ab " (2), "сразу после " (3) и "сразу после " (4).
Читая очередную букву, мы переходим в следующее состояние
по правилу, указанному в таблице.
| Текущее состояние | Очередная буква | Новое состояние |
|---|---|---|
0 |
a |
1 |
0 |
кроме а |
0 |
1 |
b |
2 |
1 |
a |
1 |
1 |
кроме a, b |
0 |
2 |
c |
3 |
2 |
a |
1 |
2 |
кроме a, c |
0 |
3 |
d |
4 |
3 |
a |
1 |
3 |
кроме a, d |
0 |
Как только мы попадем в состояние 4, работа заканчивается.
Наглядно выполнение алгоритма можно представить себе так: фишка двигается из кружка в кружок по стрелкам; стрелка выбирается так, чтобы надпись на ней соответствовала очередной букве входного слова. Чтобы этот процесс был успешным, нужно, чтобы для каждой буквы была ровно одна подходящая стрелка из любого кружка.
Соответствующая программа очевидна (мы указываем новое состояние, даже если оно совпадает со старым; эти строки можно опустить):
i:=1; state:=0;
{i - первая непрочитанная буква, state - состояние}
while (i <> n+1) and (state <> 4) do begin
| if state = 0 then begin
| | if x[i] = a then begin
| | | state:= 1;
| | end else begin
| | | state:= 0;
| | end;
| end else if state = 1 then begin
| | if x[i] = b then begin
| | | state:= 2;
| | end else if x[i] = a then begin
| | | state:= 1;
| | end else begin
| | | state:= 0;
| | end;
| end else if state = 2 then begin
| | if x[i] = c then begin
| | | state:= 3;
| | end else if x[i] = a then begin
| | | state:= 1;
| | end else begin
| | | state:= 0;
| | end;
| end else if state = 3 then begin
| | if x[i] = d then begin
| | | state:= 4;
| | end else if x[i] = a then begin
| | | state:= 1;
| | end else begin
| | | state:= 0;
| | end;
| end;
end;
answer := (state = 4);
Иными словами, мы в каждый момент храним информацию о том,
какое максимальное начало нашего образца является
концом прочитанной части. (Его длина и есть то "
состояние", о котором шла речь.)
Терминология, нами используемая, такова. Слово - это любая последовательность символов из некоторого фиксированного конечного множества. Это множество называется алфавитом, его элементы - буквами. Если отбросить несколько букв с конца слова, останется другое слово, называемое началом первого. Любое слово также считается своим началом. Конец слова - то, что останется, если отбросить несколько первых букв. Любое слово считается своим концом. Подслово - то, что останется, если отбросить буквы и с начала, и с конца. (Другими словами, подслова - это концы начал, или, что то же, начала концов.)
В терминах
своим подсловом. Эта функция не является индуктивной, но
имеет
10.2.1.
Можно ли в предыдущих рассуждениях заменить слово на произвольное слово?
Решение. Нет, и проблемы связаны с тем, что в образце
могут быть повторяющиеся буквы. Пусть, например, мы ищем
вхождения слова ababc. Вот появилась буква a, за
ней идет b, за ней идет a, затем снова b.
В этот момент мы с нетерпением ждем буквы c. Однако -
к нашему разочарованию - вместо нее появляется другая
буква, и наш образец ababc не обнаружен. Однако нас
может ожидать утешительный приз: если вместо c
появилась буква a, то не все потеряно: за ней могут
последовать буквы b и c, и образец-таки будет
найден.
Вот картинка, поясняющая сказанное:$${\setlength{\tabcolsep}{.8\tabcolsep}
\begin{tabular}{llllllllllllll}
x y z a b a b a b c
\ldots\leftarrowвходное слово\\
a b a b c
\leftarrowмы ждали
образца здесь\\
a b a b c
\leftarrowа он
оказался здесь\\
\end{tabular}$$
Таким образом, к моменту$${\setlength{\tabcolsep}{.8\tabcolsep}
\begin{tabular}{lllllll|lllllll}
x y z a b a b
\leftarrowвходное слово\\
a b a b c
\leftarrowмы ждали
образца здесь\\
a b a b c
\leftarrowа он
оказался здесь\\
\end{tabular}$$
есть два возможных положения образца, каждое из которых
подлежит проверке. Тем не менее по-прежнему возможен
конечный
10.2.2.
Указать состояния соответствующего
Решение. По-прежнему состояния будут соответствовать
наибольшему началу образца, являющемуся концом прочитанной
части слова. Их будет шесть: 0, 1 (a), 2 (ab),
3 (aba), 4 (abab), 5 (ababc). Таблица перехода
такая.
| Текущее состояние | Очередная буква | Новое состояние |
|---|---|---|
0 |
a |
1 (a) |
0 |
кроме а |
0 |
1 (a) |
b |
2 (ab) |
1 (a) |
a |
1 (a) |
1 (a) |
кроме a, b |
0 |
2 (ab) |
a |
3 (aba) |
2 (ab) |
кроме a |
0 |
3 (aba) |
b |
4 (abab) |
3 (aba) |
a |
1 (a) |
3 (aba) |
кроме a, b |
0 |
4 (abab) |
c |
5 (ababc) |
4 (abab) |
a |
3 (aba) |
4 (abab) |
кроме a, c |
0 |
Для проверки посмотрим, к примеру, на вторую снизу строку.
Если прочитанная часть кончалась на abab, а затем
появилась буква a, то теперь прочитанная часть
кончается на ababa. Наибольшее начало образца
( ababc ), являющееся ее концом - это aba.
Философский вопрос: мы говорили, что трудность
состоит в том, что есть несколько возможных положений
образца, каждое из которых может оказаться
Философский ответ. Дело в том, что самое длинное из них определяет все остальные - это его концы, одновременно являющиеся его началами.
Не составляет труда для любого конкретного образца написать
программу, осуществляющую поиск этого образца описанным
способом. Однако хотелось бы написать программу, которая
ищет произвольный образец в произвольном слове. Это можно
делать в два этапа: сначала по образцу строится таблица
переходов конечного
Для произвольного слова $$X$$ рассмотрим все его начала, одновременно являющиеся его концами, и выберем из них самое длинное. (Не считая, конечно, самого слова $$X$$.) Будем обозначать его $$l(X)$$.
Примеры: $$l({aba})={a}$$, $$l({abab})={ab}$$, $$l({ababa})={aba}$$, $$l({abc}) = \text{пустое слово}$$
10.3.1. Доказать, что все слова $$l(X)$$, $$l(l(X))$$, $$l(l(l(X)))$$ и т.д. являются началами слова $$X$$.
Решение. Каждое из них (согласно определению) является началом предыдущего.
По той же причине все они являются концами слова $$X$$.
10.3.2. Доказать, что последовательность предыдущей задачи обрывается (на пустом слове).
Решение. Каждое слово короче предыдущего.
10.3.3. Доказать, что любое слово, одновременно являющееся началом и концом слова $$X$$ (кроме самого $$X$$ ) входит в последовательность $$l(X), l(l(X)),\ldots$$
Решение. Пусть слово $$Y$$ есть одновременно начало
и конец $$X$$. Слово $$l(X)$$ - самое длинное из таких слов,
так что $$Y$$ не длиннее $$l(X)$$. Оба эти слова являются
началами $$X$$, поэтому более короткое из них является
началом более длинного: $$Y$$ есть начало $$l(X)$$.
Аналогично, $$Y$$ есть конец $$l(X)$$. Рассуждая по
l[i] есть длина наибольшего начала слова $${x[1]}\ldots{x[i]}$$, одновременно являющегося его
концом.
10.4.1.
Какое отношение все это имеет к поиску подслова? Другими
словами, как использовать алгоритм КМП для определения
того, является ли слово A подсловом слова B?
Решение. Применим алгоритм КМП к слову A\#B, где \# - специальная буква, не встречающаяся ни в A, ни
в B. Слово A является подсловом слова B тогда
и только тогда, когда среди чисел в массиве l будет
число, равное длине слова A.
10.4.2. Описать алгоритм заполнения таблицы $${l[1]}\ldots{l[n]}$$.
Решение. Предположим, что первые i значений $${l[1]}\ldots{l[i]}$$ уже найдены. Мы читаем очередную
букву слова (т.е. x[i+1] ) и должны вычислить l[i+1].
Другими словами, нас интересуют начала $$Z$$ слова $${x[1]}\ldots{x[i+1]}$$, одновременно являющиеся его
концами - из них нам надо выбрать самое длинное. Откуда
берутся эти начала? Каждое из них (не считая пустого)
получается из некоторого слова $$Z'$$ приписыванием буквы x[i+1]. Слово $$Z'$$ является началом и концом слова $${x[1]}\ldots{x[i]}$$. Однако не любое слово, являющееся
началом и концом слова $${x[1]}\ldots{x[i]}$$, годится -
надо, чтобы за ним следовала буква x[i+1].
Получаем такой рецепт отыскания слова $$Z$$. Рассмотрим все
начала слова $${x[1]}\ldots{x[i]}$$, являющиеся
одновременно его концами. Из них выберем подходящие - те,
за которыми идет буква $${x[i+1]}$$. Из подходящих выберем
самое длинное. Приписав в его конец x[i+1], получим
искомое слово $$Z$$.
Теперь пора воспользоваться сделанными нами приготовлениями и вспомнить, что все слова, являющиеся одновременно началами и концами данного слова, можно получить повторными применениями к нему функции $$l$$ из предыдущего раздела. Вот что получается:
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.
Доказать, что число действий в приведенном только что
алгоритме не превосходит $$C{n}$$ для некоторой
Решение. Это не вполне очевидно: обработка каждой
очередной буквы может потребовать многих по крайней мере на 1, и в этом случае l[i+1] окажется заметно меньше l[i]. С другой
стороны, при увеличении i на единицу величина l[i]
может возрасти не более чем на 1, так что часто
и сильно убывать она не может - иначе убывание не будет
скомпенсировано возрастанием.
Более точно, можно записать неравенство$${l[i+1]} \le {l[i]} - \hbox{(число итераций на {i}-м шаге)} + {1}$$
или$$\hbox{(число итераций на {i}-м шаге)}\le{l[i]}-{l[i+1]} + {1}.$$
Остается сложить эти i и получить
оценку сверху для общего числа
10.4.4.
Будем использовать этот алгоритм, чтобы выяснить, является
ли слово X длины n подсловом слова Y
длины m. (Как это делать с помощью специального
разделителя \#, описано выше.) При этом число действий
будет не более $$C({n}+{m})$$, и используемая память
тоже. Придумать, как обойтись памятью не более $$C{n}$$
(что может быть существенно меньше, если искомый образец
короткий, а слово, в котором его ищут - длинное).
Решение. Применяем алгоритм КМП к слову $${A#B}$$. При
этом X длины n и запоминаем эти
значения. Дальше мы помним только значение l[i] для
текущего i - кроме него и кроме таблицы $${l[1]}\ldots{l[n]}$$, нам для вычислений ничего не
нужно.
На практике слова X и Y могут не находиться подряд,
поэтому просмотр слова X и затем слова Y удобно
оформить в виде разных циклов. Это избавляет также от
хлопот с разделителем.
10.4.5. Написать соответствующий алгоритм (проверяющий, является ли слово $${X}={x[1]}\ldots{x[n]}$$ подсловом слова $${Y}={y[1]}\ldots{y[m]}$$ ).
Решение. Сначала вычисляем таблицу $${l[1]}\ldots{l[n]}$$ как раньше. Затем пишем такую программу:
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}
Этот алгоритм делает то, что на первый взгляд кажется невозможным:
в типичной ситуации он читает лишь небольшую часть всех
букв слова, в котором ищется заданный образец. Как так
может быть? Идея проста. Пусть, например, мы ищем образец . Посмотрим на четвертую букву слова: если,
к примеру, это буква e, то нет никакой необходимости
читать первые три буквы. (В самом деле, в образце
буквы e нет, поэтому он может начаться не раньше пятой
буквы.)
Мы приведем самый простой вариант этого алгоритма, который
не гарантирует быстрой работы во всех случаях. Пусть $${x[1]}\ldots{x[n]}$$ - образец, который надо искать.
Для каждого символа s найдем самое правое его вхождение
в слово X, то есть наибольшее k, при котором $${x[k]}={s}$$. Эти сведения будем хранить в массиве ; если символ s вовсе не встречается, то нам
будет удобно положить $${pos[s]}={0}$$ (мы увидим дальше,
почему).
10.5.1.
Как заполнить ?
Решение.
положить все 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[n] <> y[last] then begin {последние буквы разные}
| | last := last + (n - pos[y[last]]);
| | {n - pos[y[last]] - это минимальный сдвиг образца,
| | при котором напротив y[last] встанет такая же
| | буква в образце. Если такой буквы нет вообще,
| | то сдвигаем на всю длину образца}
| end else begin
| | если нынешнее положение подходит, т.е. если
| | x[1]..x[n] = y[last-n+1]..y[last],
| | то сообщить о совпадении;
| | last := last+1;
| end;
end;
Знатоки рекомендуют проверку совпадения проводить справа
налево, т.е. начиная с последней буквы образца (в которой
совпадение заведомо есть). Можно также немного сэкономить,
произведя ,
а n-, т.е. число букв в образце справа от
последнего вхождения буквы s.
Возможны разные модификации этого алгоритма. Например,
можно строку last:=last+1 заменить на last:=last+(n-u), где u - x[n] в образец.
10.5.2. Как проще всего учесть это в программе?
Решение. При построении таблицы написать
for i:=1 to n-1 do...
(далее как раньше), а в last:=last+1 написать
last:= last+n-pos[y[last]];
Приведенный нами упрощенный вариант алгоритма
Бойера-Мура в некоторых случаях требует существенно
больше n действий (число действий порядка mn ),
проигрывая
10.5.3.
Привести пример ситуации, в которой образец не входит
в слово, но алгоритму требуется порядка mn действий,
чтобы это установить.
Решение. Пусть образец имеет вид $${baaa}\ldots{aa}$$,
а само слово состоит только из букв a. Тогда на каждом
шаге несоответствие выясняется лишь в последний момент.
Настоящий (не упрощенный) алгоритм Бойера-Мура
гарантирует, что число действий не превосходит $$C({m}+{n})$$ в худшем случае. Он использует идеи,
близкие к идеям
Этот алгоритм основан на простой идее. Представим себе, что в слове длины $$m$$ мы ищем образец длины $$n$$. Вырежем окошечко размера $$n$$ и будем двигать его по входному слову. Нас интересует, не совпадает ли слово в окошечке с заданным образцом. Сравнивать по буквам долго. Вместо этого фиксируем некоторую функцию, определенную на словах длины $$n$$. Если значения этой функции на слове в окошечке и на образце различны, то совпадения нет. Только если значения одинаковы, нужно проверять совпадение по буквам.
Что мы выигрываем при таком подходе? Казалось бы, ничего -
ведь чтобы вычислить
10.6.1. Привести пример удобной для вычисления функции.
Решение. Заменим все буквы в слове и образце их номерами,
представляющими собой
Для каждой функции существуют слова, к которым она применима плохо. Зато другая функция в этом случае может работать хорошо. Возникает идея: надо запасти много функций и в начале работы алгоритма выбирать из них случайную. (Тогда враг, желающий подгадить нашему алгоритму, не будет знать, с какой именно функцией ему бороться.)
10.6.2. Привести пример семейства удобных функций.
Решение. Выберем некоторое число $$p$$ (желательно простое,
смотри далее) и некоторый
Следующее соображение говорит в пользу того, что совпадения
не слишком
Мы можем искать не конкретное слово, а подслова заданного
вида. Например, можно искать слова вида a?b, где
вместо ? может стоять любая буква (иными словами, нас
интересует буква b на a ).
10.7.1.
Указать конечный a?b.
Решение. Читая слово, следует помнить, есть ли буква a
на последнем месте и на предпоследнем - пока не встретим
искомый фрагмент. 00, 01, 10, 11,
их смысл таков:$$\begin{tabular}{rl}
00 на предпоследнем и последнем местах нет {a}\\
01 на предпоследнем нет, на последнем есть\\
10 не предпоследнем есть, на последнем нет\\
11 есть и там, и там
\end{tabular}$$
Таблица переходов
Другой стандартный знак в образце - это звездочка ( * ),
на место которой может быть подставлено любое слово.
Например, образец ab* означает, что мы ищем подслово ab, за которым следует что угодно, а затем (на любом
.
10.7.2.
Указать конечный ab* (в описанном только что смысле).
Решение.$$\raisebox{\depth}{\begin{tabular}{|c|c|c|} \hline Текущее Очередная Новое \\ состояние буква состояние \\ \hline начальное {a} {a} \\ начальное не {a} начальное \\ a {b} {ab} \\ a {a} {a} \\ a не {a} и не {b} начальное \\ {ab} {c} {abc} \\ {ab} не {c} {ab} \\ {abc} {d} найдено \\ {abc} {c} {abc} \\ {abc} не {c} и не {d} {ab} \\ \hline \end{tabular}}$$
Еще один вид поиска - это поиск любого из слов некоторого списка.
10.7.3.
Дан список слов $$X_1,\ldots,X_k$$ и слово $$Y$$. Определить,
входит ли хотя бы одно из слов $$X_i$$ в слово $$Y$$ (как
подслово). Количество действий не должно превосходить
Решение. Очевидный способ состоит в том, чтобы каждое слово из списка проверять отдельно (с помощью одного из рассмотренных алгоритмов). Однако при этом мы не укладываемся в заданное число действий (из-за умножения $$k$$ на длину слова $$Y$$ ).
Посмотрим на дело с другой стороны. Каждому образцу из
списка соответствует конечный
Вспомним
Склеим все образцы в
Формально говоря, вершинами
Читая входное слово, мы двигаемся по этому дереву: текущая вершина - это наибольшая (самая правая) из вершин, являющихся концом прочитанной части ( $${}={}$$ наибольший конец прочитанной части, являющийся началом одного из образцов).
Определим функцию $$l$$, аргументами и значениями которой
являются вершины
10.7.4.
Пусть $$P$$ - вершина
Решение. См.
Теперь ясно, что нужно делать, находясь в вершине $$P$$
и читая букву z входного слова. Надо просматривать
последовательно вершины $$P$$, $$l(P)$$, $$l(l(P)),..$$., пока
не обнаружится такая, из которой выходит стрелка
с буквой z. Та вершина, в которую эта стрелка ведет,
и будет нашим следующим положением.
Остается понять, как для каждой вершины
Можно поинтересоваться, какие свойства слов распознаются
с помощью конечных
Пусть фиксирован конечный (, ), * и | (они будут использоваться для
построения
Г - A,B,C,...,E - (ABC ...E) - A,B,C,...,E - (A|B|C|...|E) - A - A* - Каждое
(ABC ...E)
соответствует множество всех слов, которые можно
получить, если к слову из A приписать слово
из B, затем из C,..., затем
из E(A|B|C|...|E) соответствует
A,B,C,...,E ;A{*} соответствует
итерация множества, соответствующего
выражению A, то есть множество всех слов,
которые можно так разрезать на куски, что каждый
кусок принадлежит множеству, соответствующему
выражению A. (В частности, пустое слово всегда
содержится в A*.)Множества, соответствующие регулярным выражениям,
Называются
10.7.5.
Написать a и b, в которых число
букв a четно.
Решение. Выражение b* задает все слова без
буквы a, а выражение $${(b*}\,{a}\,{b*}\,{a}\,{b*)}$$ - все слова ровно
с двумя буквами a. Остается объединить эти множества,
а потом применить
10.7.6.
Написать bac является подсловом.
Решение. $${((a|b|c)*}\,{bac}\,{(a|b|c)*)}$$
10.7.7.
Написать bac не является подсловом.
Указание.
Эта задача сложнее предыдущей; видимо, самый простой способ
ее решить - перейти к конечным
Теперь задачу о поиске образца в слове можно
переформулировать так: проверить, принадлежит ли слово
множеству, соответствующему данному
10.7.8.
Какие выражения соответствуют образцам a?b и ab*,
рассмотренным ранее? (В образце символ * используется
не в том смысле, что в регулярных выражениях!)
Предполагается, что
Решение.
((a|b|c|d|e)*a(a|b|c|d|e)b(a|b|c|d|e)*) ((a|b|c|d|e)*ab(a|b|c|d|e)*cd(a|b|c|d|e)*)
10.7.9.
Доказать, что для всякого
Решение. Нам потребуется новое понятие - понятие
Будем двигаться различными способами из Н в К, читая буквы по дороге (на тех стрелках, где они есть). Каждому пути из Н в К, таким образом, соответствует некоторое слово. А источнику в целом соответствует множество слов - тех слов, которые можно прочесть на путях из Н в К.
Замечание. Если нарисовать состояния конечного
Мы будем строить
10.7.10.
По
Решение.
Нарисована картинка для
Наконец,
10.7.11.
Дан источник. Построить конечный
Решение. Состояниями
Тем самым задача решена.
Оказывается, что
10.7.12.
Дан источник. Построить
Решение. Пусть источник имеет вершины $$1,\ldots,k$$. Будем считать, что $$1$$ - это начало, а $$k$$ - конец. Через $$D_{i,j,s}$$ обозначим множество всех слов, которые можно прочесть на пути из $$i$$ в $$j$$, если в качестве промежуточных пунктов разрешается использовать только вершины $$1,\ldots,s$$. Согласно определению, источнику соответствует множество $$D_{1,k,k}$$.
Из чего состоит множество $$D_{i,j,s+1}$$? Отметим на пути
моменты, в которых он заходит в $$(s+1)$$ -ую вершину. При
этом путь разбивается на части, каждая из которых уже не
заходит в нее. Поэтому легко сообразить, что$$D_{i,j,s+1} =
D_{i,j,s}\,|\, (D_{i,s+1,s}\;\;\; D_{s+1,s+1,s}{*}\;\;\; D_{s+1,j,s})$$
(вольность записи: мы используем для операций над
множествами обозначения как для
10.7.13. Где еще используется то же самое рассуждение?
Ответ. В
10.7.14.
Доказать, что класс множеств, задаваемых регулярными
выражениями, не изменился бы, если бы мы разрешили
использовать не только
Решение. Для
Замечание. На практике важную роль играет число состояний
До сих про наши программы сначала получали образец, который надо искать, а потом текст, в котором надо искать. В следующих задачах все наоборот.
10.8.1.
Программа получает на вход слово $$Y$$ длины $$m$$ и может
его
обрабатывать (пока без ограничений на время и память).
Затем она получает слово $$X$$ длины $$n$$ и должна сообщить,
является ли оно подсловом слова $$Y$$. При этом число
операций при обработке слова $$X$$ должно быть порядка $$n$$
(не превосходить $$cn$$, где
Решение. Пока не накладывается никаких ограничений на
время и память при обработке $$Y$$, это не представляет
труда. Именно, надо склеить все подслова слова $$Y$$
в
Пусть такое
Заметим, что аналогичная конструкция годится для любого
множества слов $$U$$, а не только для множества всех подслов
данного слова: после того как соответствующее
10.8.2. Решить предыдущую задачу с дополнительным ограничением: объем используемой памяти пропорционален длине слова $$Y$$.
Решение. Прежний способ не годится: число вершин
Вот что получится при сжатии нашего примера:
Будем считать (здесь и далее), что последняя буква
слова $$Y$$ больше в нем не встречается. (Этого всегда можно
достичь, дописав дополнительный фиктивный символ.) Тогда
листья сжатого
У каждой внутренней вершины (не листа) сжатого
Построенное нами сжатое
10.8.3.
Показать, что построение
Решение. Будем добавлять в
Если это произойдет посередине
Гораздо более сложной задачей является построение
Для начала опишем более подробно структуру
Мы рассматриваем
Каждой вершине $$v$$ такого
Помимо вершин
Пусть $$p$$ - произвольная
Заметим, что при этом может образоваться новая вершина
(если развилка оказалась внутри
Оказывается, что навигацию в
10.8.4.
Пусть для данной позиции $$p$$ и слова $$w$$ заранее
известно, что в
Решение. В самом деле, при
Подведем итоги. Рассмотренный способ хранения деревьев позволяет (для фиксированного слова $$Y$$ )
O (число ребер на пути)];O (число букв в w, не вошедших
в l(q) ];В квадратных скобках указано число действий при выполнении соответствующих операций.
Еще мы будем хранить в вершинах
Начнем с полного (не
Вот как выглядят эти переходы в нашем примере (отрезание первой буквы соответствует пунктирной стрелке):
Эти стрелки мы будем называть
Формально можно сказать так. Пусть $$w'$$ означает
слово $$w$$
без первой буквы ( $$w'$$ определено для любого непустого
слова $$w$$ ). Тогда
10.8.5.
Как связаны
Ответ. Они указывают на соседние вершины, и буква на
соединяющем их
10.8.6.
Доказать, что при переходе к
Решение. В самом деле, по нашему предположению последняя
буква слова больше в нем не встречается, поэтому из листа
ссылка ведет в лист. А если вершина (отличная от корня)
является точкой
Вот что получится для нашего примера:
Теперь мы уже готовы к изложению алгоритма МакКрейта.
10.8.7.
Показать, что
Решение.
В самом деле,
Надо понять, как поддерживать это при добавлении очередного
Какие тут возможны
Оба варианта предполагают, что отец $$u$$ листа $$\textit{last}$$ не совпадает с корнем. (Если совпадает, нам придется добавлять $$Y_{i+1}$$ от корня.) Пусть $$\textit{tail}$$ - пометка листа $$\textit{last}$$, а $$\textit{head}=s(u)$$ ; другими словами, слово $$\textit{head}$$ соответствует вершине $$u$$. Тогда$$Y_i = \textit{head}+\textit{tail}.$$ Отрезая первую букву, получаем$$Y_{i+1}=\textit{head}\,'+\textit{tail}.$$
Заметим, что $$\textit{head}\,'$$ заведомо не выходит за
пределы
Поэтому мы можем сначала проследить $$\textit{head}\,'$$ (найти позицию $$v$$, для которой $$s(v)=\textit{head}\,'$$ ), а потом уже добавить $$\textit{tail}$$, начиная с $$v$$.
Первый способ
Эта
Тогда
$$Y_{i}=s(v)+p,\quad Y_{i+1}=Y_{i}'=s(v)'+p= s(w)+p,$$и для добавления $$Y_{i+1}$$ в
Второй способ
Итак, мы можем описать действия, выполняемые при добавление
очередного
Второй способ
Пусть $$u$$ - отец листа $$\textit{last}$$,
соответствующего
последнему уже добавленному
Случай 1: $$u$$ есть
Случай 2: $$u$$ не есть
Случай 3: $$u$$ не есть
Остается еще понять, как поддерживать структуру суффиксных ссылок (адрес нового листа у нас получается при добавлении сам собой, так что с ним проблем нет) и оценить число действий при выполнении этих процедур последовательно для всех суффиксов.
Начнем с суффиксных ссылок. По правилам они должны быть
у всех
Все сказанное можно условно записать в виде такого
алгоритма добавления
{ дерево содержит суффиксы Y1,...,Yi
s(last)=Yi
имеются корректные суффиксные ссылки для всех
внутренних вершин, кроме отца листа last}
u := отец листа last;
tail := пометка листа last;
Yi=s(u)+tail
if u=корень дерева then begin
| {Yi+1=tail'}
| добавить tail', начиная с корня,
| полученный лист поместить в last
end else begin}
| v := отец вершины u;
| pretail := пометка вершины u;
| {Yi=s(v)+pretail}+tail}
| if v = корень дерева then begin
| | {Yi+1=pretail'+tail}
| | проследить pretail' из корня в z
| end else begin
| | w := суффиксная ссылка вершины v;
| | s(w)=s(v)', Yi+1=s(w)+pretail+tail}
| | проследить pretail из w в z
| end;
| {осталось добавить tail из z и ссылку из u в z}
| if позиция z является вершиной then begin
| | поместить в u ссылку на z;
| | добавить tail, начиная с z,
| | полученный лист поместить в last;
| end else begin
| добавить tail, начиная с z,
| полученный лист поместить в last;
| поместить в u ссылку на отца листа last;
| end
end;
Осталось оценить число действий, которые выполняются при
последовательном добавлении суффиксов $$Y_1,\ldots, Y_m$$.
При добавлении каждого следующего
Прослеживания. Длительность прослеживания
пропорциональна числу $$k$$ задействованных в нем ребер, но
при этом
Добавления. Рассуждаем аналогично, но следим не за высотой последнего листа, а за длиной его пометки. При добавлении слова $$\textit{tail}$$ (или $$\textit{tail}\,'$$ ) число действий пропорционально числу просмотренных букв, но каждая просмотренная буква (кроме, быть может, одной) уменьшает длину пометки хотя бы на единицу: в пометке остаются лишь непросмотренные буквы (не считая первой). Поэтому на все добавления уходит в общей сложности $$O(m)$$ действий.
Тем самым мы доказали, что описанный алгоритм строит сжатое
10.8.8.
Как модифицировать алгоритм построения
Решение. Каждая вершина
10.8.9. Как модифицировать этот алгоритм, чтобы для каждого подслова можно было бы указывать его первое (самое левое) вхождение?
Указание.
При возникновении новой вершины на
10.8.10. Как модифицировать этот алгоритм, чтобы для каждого подслова можно было бы указывать его последнее (самое правое) вхождение?
Указание.
Если при каждом проходе корректировать информацию вдоль
пути, это будет долго; быстрее построить
10.8.11.
Как использовать
Решение. Такое подслово является внутренней вершиной
На практике можно использовать также и другой способ
нахождения самого длинного подслова, входящего дважды, -
так называемый
Этот способ требует меньше памяти (нам нужен лишь один
10.8.12. Применить один из таких алгоритмов к любимой книге и объяснить результат.
Указание. Длинные повторяющиеся куски могут быть художественным приемом (как в известном стишке про дом, который построил Джек) или следствием забывчивости автора. Для современных авторов возможно также неумеренное использование функций вырезания и вставки (заливки текста в мышь и выливания из мыши, если использовать графический интерфейс) в текстовом редакторе.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.