Алгоритм МакКрейта — различия между версиями
Genyaz (обсуждение | вклад) (Исправлен псевдокод) |
Genyaz (обсуждение | вклад) (Изменен псевдокод) |
||
Строка 51: | Строка 51: | ||
В вершинах дерева <tex>Node</tex> будем хранить следующую информацию: | В вершинах дерева <tex>Node</tex> будем хранить следующую информацию: | ||
* <tex>parent</tex> {{---}} предок | * <tex>parent</tex> {{---}} предок | ||
− | * <tex>start, | + | * <tex>s[start, end]</tex> {{---}} метка подстроки на ребре от предка |
− | * <tex>length</tex> {{---}} длина ребра до предка | + | * <tex>length = end - start + 1</tex> {{---}} длина ребра до предка |
* <tex>depth</tex> {{---}} глубина вершины в символах | * <tex>depth</tex> {{---}} глубина вершины в символах | ||
* <tex>suf</tex> {{---}} суффиксная ссылка | * <tex>suf</tex> {{---}} суффиксная ссылка | ||
Строка 75: | Строка 75: | ||
Node addSuffix(Node head, '''int''' start): | Node addSuffix(Node head, '''int''' start): | ||
newHead = slowScan(fastScan(head), start) | newHead = slowScan(fastScan(head), start) | ||
− | + | добавим новый лист к newHead | |
− | |||
'''return''' newHead | '''return''' newHead | ||
Node fastScan(Node head): | Node fastScan(Node head): | ||
− | + | если head {{---}} корень: | |
'''return''' head | '''return''' head | ||
− | + | если не существует суффиксной ссылки head: | |
skipped = head.length | skipped = head.length | ||
curPos = head.start | curPos = head.start | ||
− | + | если head совпадает с корнем: | |
skipped-- | skipped-- | ||
curPos++ | curPos++ | ||
curNode = head.parent.suf | curNode = head.parent.suf | ||
− | + | пока непройденная длина больше длины ребра: | |
curNode = curNode.children[s[curPos]] | curNode = curNode.children[s[curPos]] | ||
skipped -= curNode.length | skipped -= curNode.length | ||
curPos += curNode.length | curPos += curNode.length | ||
− | + | если остались непройденные символы: | |
− | + | разделим ребро и запишем новую вершину в curNode | |
− | head.suf = | + | head.suf = curNode |
'''return''' head.suf | '''return''' head.suf | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
Node slowScan(Node node, '''int''' start): | Node slowScan(Node node, '''int''' start): | ||
curNode = node | curNode = node | ||
curPos = start + node.depth | curPos = start + node.depth | ||
− | + | пока существует ребро по символу curPos: | |
child = curNode.children[s[curPos]] | child = curNode.children[s[curPos]] | ||
edgePos = 0 | edgePos = 0 | ||
− | + | пока символы на ребре совпадают с суффиксом: | |
curPos++ | curPos++ | ||
edgePos++ | edgePos++ | ||
− | + | если ребро пройдено до конца: | |
curNode = child | curNode = child | ||
− | + | иначе: | |
− | curNode | + | разделим ребро в месте несовпадения, запишем в curNode и выйдем из цикла |
− | |||
'''return''' curNode | '''return''' curNode | ||
</code> | </code> | ||
Строка 146: | Строка 135: | ||
* [http://www.academia.edu/3146231/Algorithms_on_strings_trees_and_sequences_computer_science_and_computational_biology ''Gusfield, Dan'' , Algorithms on Strings, Trees and Sequences: Computer Science and Computational Biology // Cambridge University Press, {{---}} 1999. {{---}} ISBN: 0-521-58519-8] | * [http://www.academia.edu/3146231/Algorithms_on_strings_trees_and_sequences_computer_science_and_computational_biology ''Gusfield, Dan'' , Algorithms on Strings, Trees and Sequences: Computer Science and Computational Biology // Cambridge University Press, {{---}} 1999. {{---}} ISBN: 0-521-58519-8] | ||
* [http://users-cs.au.dk/cstorm/courses/StrAlg_f12/slides/suffix-tree-construction.pdf ''C. N. Storm'', McCreight's suffix tree construction algorithm] | * [http://users-cs.au.dk/cstorm/courses/StrAlg_f12/slides/suffix-tree-construction.pdf ''C. N. Storm'', McCreight's suffix tree construction algorithm] | ||
− | |||
== См. также == | == См. также == |
Версия 20:53, 6 мая 2014
Алгоритм МакКрейта — алгоритм построения суффиксного дерева для заданной строки за линейное время. Отличается от алгоритма Укконена тем, что добавляет суффиксы в порядке убывания длины.
Содержание
Историческая справка
Первым оптимальным по времени был алгоритм, предложенный Вайнером в 1973 году. Идея алгоритма была в нахождении первых символов суффикса, которые находились в уже построенном дереве. Суффиксы просматривались от самого короткого к самому длинному, а для быстрого поиска использовались по два массива размера алфавита на каждую вершину, что затрудняло как понимание алгоритма, так и его реализацию и эффективность, особенно в плане занимаемой памяти. МакКрейт в 1976 году предложил свой алгоритм, в котором порядок добавления суффиксов заменен на обратный, а для быстрого вычисления места, откуда нужно продолжить построение нового суффикса, достаточно суффиксной ссылки в каждой вершине. В 1995 году Укконен представил свою версию алгоритма, которая считается наиболее простой для понимания, а также, в отличие от алгоритмов Вейнера и МакКрейта, является online алгоритмом, способным строить неявное суффиксное дерево по мере прочтения строки, а затем превратить его в настоящее.
Теоретическое обоснование
Рассмотрим строку наименьшего общего предка на этой глубине. Будем рассматривать суффиксы в порядке убывания длины, тогда имеет смысл узнавать наибольшее с новым суффиксом среди всех суффиксов, добавленных раньше. Обозначим как — максимальный префикс и среди всех .
длины , которая заканчивается специальным символом, не встречающимся больше в строке. Заметим, что если два суффикса имеют (largest common prefix) общих символов, то в построенном суффиксном дереве они будут иметьПусть мы знаем
и место в дереве, которое ему соответствует. Если позиция находится на ребре, разрежем его, а потом добавим новую вершину. Считать по определению было бы очень затруднительно, но существует способ значительно сократить вычисления.Лемма: |
Пусть , тогда — префикс . |
Доказательство: |
|
Если нам известны суффиксные ссылки
для каждой вершины , мы можем быстро перейти от позиции к ее суффиксу и продолжить сравнение символов оттуда. Если бы новая позиция всегда оказывалась существующей вершиной построенного дерева, этот алгоритм бы уже работал, но в реальности можно оказаться на середине ребра, для которой суффиксная ссылка неизвестна. Для нахождения ее суффиксной ссылки на следующей итерации мы сначала перейдем к предку, пройдем по суффиксной ссылке, а уже затем будем продолжать сравнение.Алгоритм
Для удобства реализации вместе с корнем
создадим вспомогательную вершину , обладающую свойствами:- Для любого символа из вершины есть ребро в .
Будем поддерживать инвариант:
- Для всех вершин, кроме, возможно, последней добавленной , известны суффиксные ссылки.
- Суффиксная ссылка всегда ведет в вершину, а не в середину ребра.
При добавлении каждого следующего суффикса будем выполнять следующие шаги:
- Если суффиксная ссылка
- Поднимемся вверх к ее предку;
- Пройдем по суффиксной ссылке;
- Спустимся вниз на столько символов, сколько мы прошли вверх к предку (fast scanning).
- Если мы оказались посередине ребра, разрежем его и добавим вершину.
- Установим суффиксную ссылку для
не определена:
- Иначе просто пройдем по суффиксной ссылке.
- Будем идти по дереву вниз, пока либо не будет перехода по символу, либо очередной символ на ребре не совпадет с символом нового суффикса (slow scanning)
- Добавим ребро/разрежем существующее, запомним новую позицию и добавим оставшуюся часть суффикса в качестве листа.
Утверждение: |
Инвариант алгоритма сохраняется |
Инвариант мог бы нарушиться только в случае, если бы не существовало вершины в суффиксной ссылке для Покажем, что это невозможно. Рассмотрим, что значит, что , но мы продолжили бы сканирование по ребру дальше и получили две вершины с неопределенными суффиксными ссылками. остановилась посередине ребра. Это означает, что все суффиксы , которые дошли до этого места, имеют совпадающие следующие символы, по определению отличающиеся от символа суффикса . Тогда и должен отличаться в этом символе, значит . |
Псевдокод
В вершинах дерева
будем хранить следующую информацию:- — предок
- — метка подстроки на ребре от предка
- — длина ребра до предка
- — глубина вершины в символах
- — суффиксная ссылка
- — массив детей
Конструктор будет иметь вид Node(Node parent, int start, int end, int depth)
.
Пусть глобально известна строка со специальным символом на конце, ее длина и используемый алфавит .
Node buildSuffixTree():
superRoot = Node(null, 0, -1, 0)
superRoot.suf = superRoot
root = Node(superRoot, 0, -1, 0)
root.suf = superRoot
for c in
:
superRoot.children[c] = root
head = root
for i = 1 to n:
head = addSuffix(head, i)
return root
Node addSuffix(Node head, int start): newHead = slowScan(fastScan(head), start) добавим новый лист к newHead return newHead
Node fastScan(Node head): если head — корень: return head если не существует суффиксной ссылки head: skipped = head.length curPos = head.start если head совпадает с корнем: skipped-- curPos++ curNode = head.parent.suf пока непройденная длина больше длины ребра: curNode = curNode.children[s[curPos]] skipped -= curNode.length curPos += curNode.length если остались непройденные символы: разделим ребро и запишем новую вершину в curNode head.suf = curNode return head.suf
Node slowScan(Node node, int start): curNode = node curPos = start + node.depth пока существует ребро по символу curPos: child = curNode.children[s[curPos]] edgePos = 0 пока символы на ребре совпадают с суффиксом: curPos++ edgePos++ если ребро пройдено до конца: curNode = child иначе: разделим ребро в месте несовпадения, запишем в curNode и выйдем из цикла return curNode
Асимптотическая оценка
В приведенном алгоритме используется константное число операций на добавление одного суффикса, не считая slow scanning и fast scanning.
Slow scanning делает
операций, что суммарно дает операций.Fast scanning работает с целыми ребрами, поэтому будем использовать в качестве потенциала глубину в вершинах. Из структуры суффиксного дерева мы знаем, что суффиксная ссылка может уменьшить глубину вершины не более, чем на , так что мы на каждой итерации поднимаемся не более, чем на — один раз к предку, а потом по суффиксной ссылке, что составляет за весь алгоритм. Соответственно, спустимся мы тоже суммарно раз, так как и максимальная глубина составляет .
Итоговая асимптотика алгоритма —
.Сравнение с другими алгоритмами
В сравнении с алгоритмом Вайнера:
- Преимущества: каждая вершина хранит только суффиксную ссылку, а не массивы размера алфавита.
- Недостатки: нет.
В сравнении с алгоритмом Укконена:
- Преимущества: мы строим суффиксное дерево в явной форме, что может облегчить понимание алгоритма.
- Недостатки: является offline алгоритмом, то есть требует для начала работы всю строку целиком.
Источники
- Suffix tree - Wikipedia
- Gusfield, Dan , Algorithms on Strings, Trees and Sequences: Computer Science and Computational Biology // Cambridge University Press, — 1999. — ISBN: 0-521-58519-8
- C. N. Storm, McCreight's suffix tree construction algorithm