Алгоритм Фараха
Алгоритм Фарача (Martin Farach,(1997)) — алгоритм построения суффиксного дерева для заданной строки длины . Сам алгоритм выполнятеся за время , при этом даже не требуется выполнения условия конечности алфавита. Такая эффективность достигается за счет того, что строковые последовательности определяются на индексированном алфавите или, что эквивалентно, на целочисленном алфавите , при этом накладывается дополнительное условие, что . Такие алфавиты часто встречаются на практике.
Содержание
Описание алгоритма
Основная идея алгоритма заключается в том, что мы уменьшаем размер исходной строки. Для этого мы разбиваем символы сходной строки на пару и пронумеровываем их, а из полученных номеров составляем новую строку, которая уже в
раза короче.Алгоритм Фарача будет описан в виде пяти выполняемых шагов. Используем в качестве примера строку
, определенную на алфавите (в этом примере ).Шаг 1: суффиксное дерево для сжатой строки
- Строка
- (если символов нечетное число — последняя пара дополняется специальным символом )
разбивается на пары подряд идущих символов:
- Пары сортируются устойчивой сотрировкой ( удобно сортировать поразрядной, так число разрядов мало, размер алфавита — , поэтому время работы сортировки — линейное ): .
- Удаляются копии: .
- Парам даются номера (условно, в массиве они и так есть):
- В исходной строке пары заменяются на номера:
- Из полученной строки вдвое меньшего размера рекурсивно создаётся суффикcное дерево тем же алгоритмом:
ID | LCP | STR |
---|---|---|
1 | 0 | 0 1 2 3 2 |
0 | 0 | 1 0 1 2 3 2 |
2 | 1 | 1 2 3 2 |
3 | 0 | 2 3 2 |
5 | 1 | 2 |
4 | 0 | 3 2 |
Шаг 2: построение чётного дерева
Определение: |
Четное дерево | является деревом суффиксов для строки , узлы-листья которого ограничены четными позициями строки .
Из дерева сжатой строки получаем частичное (чётное) дерево исходной строки. Частичное оно потому, что в нём будет только половина суффиксов, то есть те, которые стоят в чётных позициях.
Номер каждой пары превращается в номер четного суффикса исходной строки. Раскрываем все пары в суффиксы, а номера в листьях от этого умножатся на
очевидным образом:Корректируются все развилки дерева (так как они могут совпадать в первых символах): Lля всех внутренних вершин
, ребра всех детей которых начинаются с одинаковых символов, мы создадим новую вершину между и ее детьми. Это можно сделать быстро, так как все ребра, исходящие их любой вершины, лексикографически отсортированы по своим первым двум символам(так как мы сортировали пары номера пар на прошлом шаге). Для каждого ребра нам достоточно проверить, что его первыей символ соответствует первому символу соседнего ребра, и, если так, сделать необходимые исправления. Может быть, что ребра ко всем детям начинаются с одинакового символа, и в этом случае у вершины будет только один ребенок теперь. Ну тогда удалим . Эта процедура требует требует константное время на каждое ребро и константное время на каждую вершину, а значит, на нее требуется линейное время.
Итак, если — это время, которое птребуется нашему алгоритму, чтобы построить суффиксное дерево для строки , то может быть построено за время
ID | LCP | STR |
---|---|---|
2 | 0 | 1112212221 |
0 | 1 | 121112212221 |
4 | 2 | 12212221 |
6 | 0 | 212221 |
10 | 2 | 21 |
8 | 1 | 2221 |
Шаг 3: построение нечетного по четному
Определение: |
Нечётное дерево | является деревом суффиксов для строки , узлы-листья которого ограничены нечетными позициями строки .
Из чётного дерева нужно получить нечётное дерево (дерево из суффиксов в нечётных позициях). Мы хотим получить лексикографически отсортированные нечетные суффиксы строки, по которым воссановим нечетное дерево. Заметим, что все нечетные суффиксы представляют собой один символ, за которым дальше следует четный суффикс, которые у нас уже отсортированы. Тогда поразрядно отсортируем их за линейное время.
Таким образом
может быть построено за линейное время по .
Выяснение общего префикса строк можно находить общего предка вершин в суффиксном дереве. Такого предка можно найти за константное время, потратив . Для примера в этом дереве, общее начало строк 5 и 9 ( на препроцессинг и ) записано в пути от корня до общего предка этих вершин: (рисунок 3-1)
Поскольку структуры нечётного дерева у нас заранее нет и мы её только строим, то подходящих предков мы можем найти в исходном чётном дереве, для этого достаточно проверить вершины с номерами на единицу меньше и отрезать первый символ : (рисунок 3-2).
Шаг 4: слияние четного и нечетного дерева
Далее необходимо найти эффективный способ слияния нечетного и четного деревьев в одно дерево
. Слияние будем производить, начиная с корней деревьев. Предположим, что для каждого узла деревьев и выходящие из них ребра занесены в специальные списки, где они упорядочены в возрастающем лексикографическом порядке подстрок, которые представляют эти ребра. Возьмем по одному ребру из этих списков у каждого дерева, обрадботаем их, и рекурсивно спустимся в из поддеревья. Алгоритм просматривает только первые буквы подстрок, представленных ребрами деревьев и , пусть это будут буквы и . Тогда:- если , определяется поддерево, соответствующее меньшей из этих букв, и без изменений присоединяется к узлу-родителю;
- если и длины подстрок, представленных соответствующими ребрами, равны, в дерево слияния к текущему узлу добавляются два сына: один — из четного дерева, другой — из нечетного;
- если и длины подстрок, представленных соответствующими ребрами, различны, в дерево слияния к текущему узлу добавляются два узла, находящиеся на одном нисходящем пути, при этом ближайший узел будет соответствовать более короткой подстроке.
поскольку мы рассматриваем только первый символ каждого ребра(то есть делаем вид, что ребра равны, если первые символы у них равны), то мы может иногда слить ребра, которые не должны были быть слиты, но те, которые надо было слить, точно сольем.
Если начать эту процедуру для корней нечетного и четного деревьев, далее она рекурсивно выполняется для корней всех поддеревьев, которые, возможно, уже содержат узлы из нечетного и четного деревьев, поскольку ранее мог быть реализован случай
. Так как время манипулирования любым ребром этих деревьев фиксировано, то общее время слияния деревьев составит .В результате описанных действий получится дерево
, в котором будут присутствовать поддеревья, которые прошли процедуру слияния, и которые ее избежали (то есть были перенесены в дерево без изменений).Шаг 5: удаление двойных дуг
Разбираемся с двойными дугами (на этом примере их три). Для этого мы должны выяснить, сколько начальных символов таких дуг совпадает. Совпадать может от одного до нескольких символов, или даже все. Проверять их все по очереди нельзя (это даст квадратичное время). Если дуги совпадают полностью, тогда ничего не делаем, удаляем одну из копий и всё. Если начало для двух дуг совпадает только частично, тогда нужно делать для них общее начало, а ветки, которые на концах, снова развести по разным деревьям (для этого можно во время слияния запомнить их начальный цвет или просто сохранить ссылки на исходные ветки).
Для примера как это сделать возьмём строку
:Для того чтобы узнать общее начало двойной дуги, нужно взять одну чётную и одну нечётную на дереве, для которых родителем является конец нашей двойной дуги. Например, на рисунке выше двойная дуга
(конец помечен зелёным) является общим родителем для вершин и . Чтобы узнать, на каком расстоянии будет расслаиваться двойная дуга, надо увеличить номера вершин на единицу и найти их родителя. Он будет находиться на единицу ближе к корню (и путь у вершин будет одинаковой строкой, не считая размера). Родитель вершин и помечен жёлтым, он находится на расстоянии от корня, следовательно, дуга должна расслаиваться в двух символах от корня, то есть обе дуги совпадают и их просто надо слить.Разберём дуги по порядку:
- расслоение находится на расстоянии два от корня, то есть дуга не расслаивается.
- конец является родителем вершин , . Родитель , после слияния дуги , находится на глубине символа. Значит, дуга расслаивается на глубине символа, то есть так же не расслаивается. Дугу нужно вычислять после обработки дуги , потому что конец дуги после обработки может оказаться на разной высоте, в зависимости от того на каком символе она расслоилась.
- конец является родителем , . Родитель , находится на расстоянии , а наше расслоение на расстоянии , то есть сливается первый символ двойной дуги. Дугу надо вычислять после дуги . Потому что если на дуге появится разветвление, то компоненты дуги придётся растащить по разным веткам дерева и сравнивать их будет не нужно.
- конец является родителем , . Расслаивается на втором символе.
- конец является родителем , . Дугу можно обрабатывать только после дуги , так как от неё будет зависеть глубина расслоения.
Замечания
Как показывает автор с своей работе, чётное дерево
строится за линейное время. За линейное же время из него получается , и слияние и занимает . К недостаткам можно отнести то, что для рекурсивного построения всех и требуется памяти. В целом, алгоритм скорее теоретический, чем практический, а основная ценность его заключается в том, что размер алфавита может быть произвольным.