Алгоритм МакКрейта

Материал из Викиконспекты
Версия от 20:59, 2 мая 2014; Genyaz (обсуждение | вклад) (Новая страница: «{{В разработке}} '''Алгоритм МакКрейта''' — алгоритм построения [[Сжатое суффиксное дерево|...»)
(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск
Эта статья находится в разработке!

Алгоритм МакКрейта — алгоритм построения суффиксного дерева для заданной строки [math]s[/math] за линейное время. Отличается от алгоритма Укконена тем, что добавляет суффиксы в порядке убывания длины.

Теоретическое обоснование

Заметим, что если два суффикса имеют [math]lcp[/math] (largest common prefix) общих символов, то в построенном суффиксном дереве они будут иметь наименьшего общего предка на этой глубине. Будем рассматривать суффиксы в порядке убывания длины, тогда имеет смысл узнавать наибольшее [math]lcp[/math] с новым суффиксом среди всех суффиксов, добавленных раньше.


Определение:
[math]head_i[/math] — максимальный префикс [math]s[j..\left\vert s \right\vert][/math] и [math]s[i..\left\vert s \right\vert])[/math] среди всех [math]j \lt i[/math].


Пусть мы знаем [math]head_i[/math] и место в дереве, которое ему соответствует. Если позиция находится на ребре, разрежем его, а потом добавим новую вершину.

Считать [math]head_i[/math] по определению было бы очень затруднительно, но существует способ значительно сократить вычисления.

Лемма:
Пусть [math]head_i = s[i..i+h][/math], тогда [math]s[i+1..i+h][/math] — префикс [math]head_{i+1}[/math].
Доказательство:
[math]\triangleright[/math]
  • При [math]h=0[/math], очевидно, пустая строка является префиксом любой другой.
  • Пусть [math]h \gt 0[/math]. Из определения [math]head_i[/math] существует суффикс [math]s[j..\left\vert s \right\vert][/math], имеющий [math]h[/math] общих символов с [math]s[i..\left\vert s \right\vert][/math]. Тогда [math]s[j+1..\left\vert s \right\vert][/math] будет иметь с [math]s[i+1..\left\vert s \right\vert][/math] общий префикс длины [math]h-1[/math]. Поскольку любой общий префикс будет по определению также являться и префиксом [math]head_{i+1}[/math], получаем искомое утверждение.
[math]\triangleleft[/math]

Если нам известны суффиксные ссылки [math]u.suf[/math] для каждой вершины [math]u[/math], мы можем быстро перейти от позиции [math]head_i[/math] к ее суффиксу и продолжить сравнение символов оттуда. Если бы новая позиция [math]head_i[/math] всегда оказывалась существующей вершиной построенного дерева, этот алгоритм бы уже работал, но в реальности можно оказаться на середине ребра, для которой суффиксная ссылка неизвестна. Для нахождения ее суффиксной ссылки на следующей итерации мы сначала перейдем к предку, пройдем по суффиксной ссылке, а уже затем будем продолжать сравнение.

Алгоритм

Для удобства реализации вместе с корнем [math]root[/math] создадим вспомогательную вершину [math]superRoot[/math], обладающую свойствами:

  • [math]root.suf = superRoot[/math]
  • Для любого символа [math]c[/math] из вершины [math]superRoot[/math] есть ребро в корень.

Будем поддерживать инвариант:

  1. Для всех вершин, кроме, возможно, последней добавленной [math]head_i[/math], известны суффиксные ссылки.
  2. Суффиксная ссылка всегда ведет в вершину, а не в середину ребра.

При добавлении каждого следующего суффикса будем выполнять следующие шаги:

  • Если суффиксная ссылка [math]head_{i-1}[/math] не определена:
    1. Поднимемся вверх к ее предку;
    2. Пройдем по суффиксной ссылке;
    3. Спустимся вниз на столько символов, сколько мы прошли вверх к предку (fast scanning).
    4. Если мы оказались посередине ребра, разрежем его и добавим вершину.
    5. Установим суффиксную ссылку для [math]head_{i-1}[/math]
  • Иначе просто пройдем по суффиксной ссылке.
  • Будем идти по дереву вниз, пока либо не будет перехода по символу, либо очередной символ на ребре не совпадет с символом нового суффикса (slow scanning)
  • Добавим ребро/разрежем существующее, запомним новую позицию [math]head_i[/math] и добавим оставшуюся часть суффикса в качестве листа.
Утверждение:
Инвариант алгоритма сохраняется
[math]\triangleright[/math]

Инвариант мог бы нарушиться только в случае, если бы не существовало вершины в суффиксной ссылке для [math]head_{i-1}[/math], но мы продолжили бы сканирование по ребру дальше и получили две вершины [math]head_{i-1}.suf, head_i[/math] с неопределенными суффиксными ссылками.

Покажем, что это невозможно. Рассмотрим, что значит, что [math]head_{i-1}.suf[/math] остановилась посередине ребра. Это означает, что все суффиксы [math]s[j..\left\vert s \right\vert], j \lt i - 1[/math], которые дошли до этого места, имеют совпадающие символы, по определению [math]head_{i-1}[/math] отличающиеся от символа суффикса [math]s[i - 1..\left\vert s \right\vert][/math]. Тогда и [math]s[i..\left\vert s \right\vert][/math] должен отличаться в этом символе, значит [math]head_i = head_{i-1}.suf[/math].
[math]\triangleleft[/math]

Асимптотическая оценка

В приведенном алгоритме используется константное число операций на добавление одного суффикса, не считая slow scanning и fast scanning.

Slow scanning делает [math]\left\vert head_i \right\vert - \left\vert head_{i-1} \right\vert + 1[/math] операций, что суммарно дает [math]\left\vert head_n \right\vert - \left\vert head_0 \right\vert + n = O(n)[/math] операций.

Fast scanning работает с целыми ребрами, поэтому будем использовать в качестве потенциала глубину в вершинах. Из структуры суффиксного дерева мы знаем, что суффиксная ссылка может уменьшить глубину вершины не более, чем на [math]1[/math], так что мы на каждой итерации поднимаемся не более, чем на [math]2[/math] — один раз к предку, а потом по суффиксной ссылке, что составляет [math]O(n)[/math] за весь алгоритм. Соответственно, спустимся мы тоже суммарно [math]O(n)[/math] раз, так как и максимальная глубина составляет [math]O(n)[/math].

Итоговая асимптотика алгоритма — [math]O(n)[/math].

Сравнение с другими алгоритмами

Является первым асимптотически оптимальным алгоритмом. В сравнении с алгоритмом Укконена:

  • Преимущества: мы строим суффиксное дерево в явной форме, что может облегчить понимание алгоритма.
  • Недостатки: является offline алгоритмом, то есть требует для начала работы всю строку целиком.

Источники

См. также