Опубликован: 08.04.2009 | Уровень: для всех | Доступ: платный
Лекция 10:

Сопоставление с образцом

Надо понять, как поддерживать это при добавлении очередного суффикса. Можно, не мудрствуя лукаво, добавлять Y_{i+1} буква за буквой, начиная с корня дерева. (Именно так мы раньше и делали, и это требовало квадратичного времени.)

Какие тут возможны оптимизации? Первая связана с тем, что мы можем двигаться по дереву быстрее, если знаем, что заведомо из него не выйдем. Вторая связана с использованием суффиксных ссылок.

Оба варианта предполагают, что отец 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{u} было точкой ветвления, поэтому помимо листа Y_i через точку u проходил и лист Y_j с j<i. Тогда Y_j начинается на \textit{head}, а Y_{j+1} начинается на \textit{head}\,' и уже есть в дереве.

Поэтому мы можем сначала проследить \textit{head}\,' (найти позицию v, для которой s(v)=\textit{head}\,' ), а потом уже добавить \textit{tail}, начиная с v.


Первый способ оптимизации: \textit{head}\,' заведомо есть в дереве.

Эта оптимизация никак не использует суффиксных ссылок. Второй способ оптимизации их использует и позволяет (в том случае, когда применим) обойтись без прослеживания Y_{i+1} от корня (как ускоренного, так и обычного). Пусть на пути к листу \textit{last}, представляющему суффикс Y_i, имеется вершина v, у которой суффиксная ссылка указывает на вершину w, так что s(w)=s(v)'. Пусть p - слово на пути от v к \textit{last}.

Тогда

Y_{i}=s(v)+p,\quad Y_{i+1}=Y_{i}'=s(v)'+p= s(w)+p,

и для добавления Y_{i+1} в дерево достаточно добавить слово p, начиная с вершины w.

Второй способ оптимизации сочетается с первым: отрезок слова p от v до отца листа \textit{last} можно проходить с уверенностью, что мы не выйдем за пределы дерева.

Итак, мы можем описать действия, выполняемые при добавление очередного суффикса Y_{i+1} в дерево, следующим образом.


Второй способ оптимизации: пользуемся суффиксной ссылкой вершины на пути к \textit{last}.

Пусть u - отец листа \textit{last}, соответствующего последнему уже добавленному суффиксу Y_i.

Случай 1: u есть корень дерева. Тогда ни одна из оптимизаций не применима, и мы добавляем Y_{i+1}, начиная от корня.

Случай 2: u не есть корень дерева, но отец u есть корень дерева (лист \textit{last} находится на высоте 2 ). Тогда Y_i=\textit{head}+\textit{tail}, где \textit{head} и \textit{tail} - пометки вершин u и \textit{last}. Мы применяем первую оптимизацию и прослеживаем \textit{head}\,' с гарантией до некоторой позиции z, а потом добавляем \textit{tail} от z.

Случай 3: u не есть корень дерева и его отец v также не есть корень дерева. Тогда для v имеется суффиксная ссылка на некоторую вершину w, и s(w)=s(v)'. Пусть \textit{pretail} - пометка вершины u, а \textit{tail} - пометка листа \textit{last}, при этом Y_i=s(v)+\textit{pretail}+\textit{tail} и потому Y_{i+1}=Y_i'=s(w)+\textit{pretail}+\textit{tail}. Остается проследить \textit{pretail} от вершины w с гарантией, получив некоторую позицию z, а потом добавить \textit{tail} от вершины z.

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

Начнем с суффиксных ссылок. По правилам они должны быть у всех внутренних вершин, кроме отца только что добавленного листа. Поэтому нам надо заботиться об отце листа \textit{last}, соответствующего Y_i (этот лист перестал быть "только что добавленным"; напротив, единственная новая вершина как раз является отцом только что добавленного листа и в ней суффиксная ссылка не нужна). Это актуально в случаях 2 и 3, но в этих случаях по ходу дела была найдена нужная вершина z, куда и будет направлена суффиксная ссылка из u. Строго говоря, z могла быть не вершиной, а позицией, но тогда после добавления она станет вершиной (отцом только что добавленного листа) - ведь в новом дереве u уже не является отцом последнего листа, и потому суффиксная ссылка из u, как было доказано, должна вести в вершину. (Другими словами, в случаях 2 и 3, если позиция z была внутри ребра, то в ней ребро разрезается.)

Все сказанное можно условно записать в виде такого алгоритма добавления суффикса Y_{i+1}:

{ дерево содержит суффиксы 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. При добавлении каждого следующего суффикса выполняется конечное число действий, если не считать действий при "прослеживании" и "добавлении". Нам надо установить, что общее число действий есть O(m) ; для этого достаточно отдельно доказать, что суммарное число действий при всех прослеживаниях есть O(m) и суммарное число действий при всех добавлениях есть O(m). (Заметим, что некоторые прослеживания или добавления могут быть долгими - но это компенсируется другими.)

Прослеживания. Длительность прослеживания пропорциональна числу k задействованных в нем ребер, но при этом высота последнего добавленного листа (число ребер на пути к нему) увеличивается на k-O(1) (по сравнению с предыдущим добавленным листом). Чтобы убедиться в этом, достаточно заметить, что в третьем случае высота вершины s(w) может быть меньше высоты вершины s(v) разве что на единицу, поскольку суффиксные ссылки из всех вершин на пути к v (не считая корня, где нет суффиксной ссылки) ведут в вершины на пути к w. Поскольку высота любого листа ограничена числом m, заключаем, что общая длительность всех прослеживаний есть O(m).

Добавления. Рассуждаем аналогично, но следим не за высотой последнего листа, а за длиной его пометки. При добавлении слова \textit{tail} (или \textit{tail}\,' ) число действий пропорционально числу просмотренных букв, но каждая просмотренная буква (кроме, быть может, одной) уменьшает длину пометки хотя бы на единицу: в пометке остаются лишь непросмотренные буквы (не считая первой). Поэтому на все добавления уходит в общей сложности O(m) действий.

Тем самым мы доказали, что описанный алгоритм строит сжатое суффиксное дерево слова Y длины m за O(m) действий. После этого для любого слова X длины n можно за O(n) действий выяснить, является ли X подсловом слова Y.

10.8.8. Как модифицировать алгоритм построения суффиксного дерева, чтобы не только узнавать, является ли данное слово X подсловом слова Y, но и (если является) указывать место, где оно встречается (одно из таких мест, если их несколько)? Время построения должно оставаться O(|Y|), время поиска подслова - O(|X|).

Решение. Каждая вершина сжатого суффиксного дерева соответствует некоторому подслову слова Y ; в момент, когда эта вершина была добавлена в дерево, известно, в каком месте есть такое подслово, и можно записать в вершине, где соответствующее подслово кончается.

10.8.9. Как модифицировать этот алгоритм, чтобы для каждого подслова можно было бы указывать его первое (самое левое) вхождение?

Указание. При возникновении новой вершины на ребре нужно брать ее первое вхождение (информация о котором есть на конце ребра), а не второе, только что обнаруженное.

10.8.10. Как модифицировать этот алгоритм, чтобы для каждого подслова можно было бы указывать его последнее (самое правое) вхождение?

Указание. Если при каждом проходе корректировать информацию вдоль пути, это будет долго; быстрее построить дерево, затем вновь его обойти и для каждой вершины вычислить момент последнего появления соответствующего подслова.

10.8.11. Как использовать сжатое суффиксное дерево, чтобы для данного слова Y за время O(|Y|) найти самое длинное подслово, которое входит в Y более одного раза?

Решение. Такое подслово является внутренней вершиной суффиксного дерева, поэтому достаточно из всех его вершин взять ту, которой соответствует самое длинное слово. Для этого достаточно обойти все его вершины (длину можно вычислять по мере обхода, складывая длины пометок на ребрах).

На практике можно использовать также и другой способ нахождения самого длинного подслова, входящего дважды, - так называемый массив суффиксов. А именно, будем рассматривать число i как "код" конца слова, начинающего с i -ой буквы. Введем на кодах порядок, соответствующий лексикографическому (словарному) порядку на словах: код i предшествует коду j, если конец слова, начинающийся с i, в лексикографическом порядке идет раньше конца слова, начинающегося с j. После этого отсортируем коды в соответствии с этим порядком, получив некоторую перестановку массива 1,2,3,\ldots,m (где m - длина исходного слова Y ). Если какое-то слово X входит в слово Y дважды, то оно является началом двух концов слова Y. При этом эти концы можно выбрать соседними в лексикографическом порядке, поскольку все промежуточные слова тоже начинаются на X. Значит, достаточно для всех соседних концов посмотреть, сколько начальных букв у них совпадает, и взять максимум.

Этот способ требует меньше памяти (нам нужен лишь один массив из целых чисел той же длины, что исходное слово), но может требовать большого времени: во-первых, сортировка сама по себе требует порядка m log m сравнений, во-вторых, каждое сравнение может длиться долго, если совпадающий кусок большой. Но в случаях, когда длинных совпадающих кусков мало, такой алгоритм работает неплохо.

10.8.12. Применить один из таких алгоритмов к любимой книге и объяснить результат.

Указание. Длинные повторяющиеся куски могут быть художественным приемом (как в известном стишке про дом, который построил Джек) или следствием забывчивости автора. Для современных авторов возможно также неумеренное использование функций вырезания и вставки (заливки текста в мышь и выливания из мыши, если использовать графический интерфейс) в текстовом редакторе.

Татьяна Новикова
Татьяна Новикова
Россия, Пошатово
Artem Bardakov
Artem Bardakov
Россия