Алгоритм Укконена — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
м (Реализация)
м (rollbackEdits.php mass rollback)
 
(не показана 131 промежуточная версия 10 участников)
Строка 4: Строка 4:
 
Рассмотрим сначала наивный метод, который строит дерево за время <tex>O(n^3)</tex>, где <tex>n</tex> — длина исходной строки <tex>s</tex>. В дальнейшем данный алгоритм будет оптимизирован таким образом, что будет достигнута линейная скорость работы.
 
Рассмотрим сначала наивный метод, который строит дерево за время <tex>O(n^3)</tex>, где <tex>n</tex> — длина исходной строки <tex>s</tex>. В дальнейшем данный алгоритм будет оптимизирован таким образом, что будет достигнута линейная скорость работы.
 
{{Определение
 
{{Определение
|definition= '''Неявное суффиксное дерево''' (англ. ''implicit suffix tree, IST'') строки <tex>S</tex> {{---}} это суффиксное дерево, построенное для строки <tex>S</tex> без добавления защитного символа.}}
+
|definition= '''Неявное суффиксное дерево''' (англ. ''implicit suffix tree, IST'') строки <tex>S</tex> {{---}} это суффиксное дерево, построенное для строки <tex>S</tex> без добавления <tex>\$</tex>.}}
 
[[Файл:ExampleUkkonen2.png|400px|thumb|right|Пример построения суффиксного дерева алгоритмом Укконена.]]
 
[[Файл:ExampleUkkonen2.png|400px|thumb|right|Пример построения суффиксного дерева алгоритмом Укконена.]]
Алгоритм последовательно строит неявные суффиксные деревья для всех префиксов исходного текста <tex>S = s_{1}s_{2}...s_{n}</tex>. На <tex>i</tex>-ой итерации неявное суффиксное дерево <tex>\tau_{i-1}</tex> для префикса <tex>s[1..i-1]</tex> достраивается до <tex>\tau_{i}</tex> для префикса <tex>s[1..i]</tex>. Будем спускаться от корня дерева до конца каждого суффикса префикса <tex>s[1..i-1]</tex> и дописывать к ним символ <tex>s_{i}</tex>. Не стоит забывать,  что <tex>s_{i}</tex> является суффиксом <tex>s[1..i]</tex> , поэтому его тоже нужно добавить в дерево. <br>
+
Алгоритм последовательно строит неявные суффиксные деревья для всех префиксов исходного текста <tex>S = s_{1}s_{2} \ldots s_{n}</tex>. На <tex>i</tex>-ой фазе неявное суффиксное дерево <tex>\tau_{i-1}</tex> для префикса <tex>s[1 \ldots i-1]</tex> достраивается до <tex>\tau_{i}</tex> для префикса <tex>s[1 \ldots i]</tex>. Достраивание происходит следующим образом: для каждого суффикса подстроки <tex>s[1 \ldots i-1]</tex> необходимо спуститься от корня дерева до конца этого суффикса и дописать символ <tex>s_i</tex>.
  
Алгоритм состоит из <tex>n</tex> итераций так как в исходном тексте <tex>O(n)</tex> суффиксов. На каждой фазе происходит продление всех суффиксов по порядку, что требует <tex>O(n^2)</tex> времени. Следовательно, общая асимптотика алгоритма <tex>O(n^3)</tex>.
+
Алгоритм состоит из <tex>n</tex> фаз. На каждой фазе происходит продление всех суффиксов текущего префикса строки, что требует <tex>O(n^2)</tex> времени. Следовательно, общая асимптотика алгоритма составляет <tex>O(n^3)</tex>.
 +
=== Псевдокод алгоритма за O(n<sup>3</sup>) ===
 +
<code style = "display: inline-block;">
 +
  '''for''' i = 1 .. n
 +
    '''for''' j = 1 .. i
 +
      treeExtend(s[j..i]) <font color=green>// добавление текущего суффикса работает за линейное время</font>
 +
</code>
 +
'''Замечание:''' на первый взгляд, более логичным подходом кажется добавление всех суффиксов строки в дерево по очереди, получив сразу алгоритм со временем работы <tex>O(n^2)</tex>. Однако осуществить улучшение данного алгоритма до линейного времени работы будет намного сложней, хотя именно в этом и заключается суть [[Алгоритм МакКрейта | алгоритма МакКрейта]].
  
 
== Продление суффиксов ==
 
== Продление суффиксов ==
Ниже приведены возможные случаи, которые могут возникнуть при добавлении символа <tex>s_{i}</tex> ко всем суффиксам префикса <tex>s[1..i-1]</tex>.
+
Ниже приведены возможные случаи, которые могут возникнуть при добавлении символа <tex>s_{i}</tex> ко всем суффиксам префикса <tex>s[1 \ldots i-1]</tex>.
{| border="1" cellpadding="5" cellspacing="0" style="text-align:center" width=75%
+
{| border="1" cellpadding="3" cellspacing="0" style="text-align:center" width=75%
 
!style="background:#f2f2f2"|Случай
 
!style="background:#f2f2f2"|Случай
 
!style="background:#f2f2f2"|Правило
 
!style="background:#f2f2f2"|Правило
Строка 18: Строка 25:
 
|-
 
|-
 
|style="background:#ffffff"|''1. Продление листа''
 
|style="background:#ffffff"|''1. Продление листа''
|style="background:#ffffff"|Пусть суффикс <tex>s[k..i-1]</tex> заканчивается в листе. Добавим <tex>s_{i}</tex> в конец подстроки, которой помечено ребро, ведущее в этот лист.
+
|style="background:#ffffff"|Пусть суффикс <tex>s[k \ldots i-1]</tex> заканчивается в листе. Добавим <tex>s_{i}</tex> в конец подстроки, которой помечено ребро, ведущее в этот лист.
 
|style="background:#ffffff"|[[Файл:ExampleUkkonen3.png|300px]]
 
|style="background:#ffffff"|[[Файл:ExampleUkkonen3.png|300px]]
 
|-
 
|-
|style="background:#ffffff"|''2.1 Создание листа''
+
|style="background:#ffffff" rowspan="2" |''2. Ответвление''
|style="background:#ffffff"|Пусть суффикс <tex>s[k..i-1]</tex> заканчивается в вершине, не являющейся листом, из которой нет пути по символу <tex>s_{i}</tex>. Создадим новый лист, в который из текущей вершины ведет дуга с пометкой <tex>s_{i}</tex>.
+
|style="background:#ffffff"|а) Пусть суффикс <tex>s[k \ldots i-1]</tex> заканчивается в вершине, не являющейся листом, из которой нет пути по символу <tex>s_{i}</tex>. Создадим новый лист, в который из текущей вершины ведёт дуга с пометкой <tex>s_{i}</tex>.
 
|style="background:#ffffff"|[[Файл:ExampleUkkonen4.png|300px]]
 
|style="background:#ffffff"|[[Файл:ExampleUkkonen4.png|300px]]
 
|-
 
|-
|style="background:#ffffff"|''2.2 Ответвление''
+
|style="background:#ffffff"|б) Пусть суффикс <tex>s[k \ldots i-1]</tex> заканчивается на ребре с меткой <tex>s[l \ldots r]</tex> в позиции <tex>p-1(l \leqslant p \leqslant r)</tex> и <tex>s_{p} \ne s_{i}</tex>. Разобьем текущее ребро новой вершиной на <tex>s[l \ldots p-1]</tex> и <tex>s[p \ldots r]</tex> и подвесим к ней еще одного ребенка с дугой, помеченной <tex>s_{i}</tex>.
|style="background:#ffffff"|Пусть суффикс <tex>s[k..i-1]</tex> заканчивается на ребре, <tex>t[1..p-1]</tex> совпадает с концом <tex>s[k..i-1]</tex> и <tex>t_{p}\ne s_{i}</tex>. Разобьем текущее ребро новой вершиной на <tex>t[1..p-1]</tex> и <tex>t[p..l]</tex>, где <tex>l</tex> {{---}} длина метки ребра, и подвесим к ней еще одного ребенка с дугой, помеченной <tex>s_{i}</tex>.
 
 
|style="background:#ffffff"|[[Файл:ExampleUkkonen5.png|300px]]
 
|style="background:#ffffff"|[[Файл:ExampleUkkonen5.png|300px]]
 
|-
 
|-
|style="background:#ffffff"|''3 Ничего не делать''
+
|style="background:#ffffff"|''3. Ничего не делать''
|style="background:#ffffff"|Пусть суффикс <tex>s[k..i-1]</tex> заканчивается в вершине, из которой есть путь по <tex>s_{i}</tex>. Тогда ничего делать не надо.
+
|style="background:#ffffff"|Пусть суффикс <tex>s[k \ldots i-1]</tex> заканчивается в вершине, из которой есть путь по <tex>s_{i}</tex>. Тогда ничего делать не надо.
 
|style="background:#ffffff"|[[Файл:ExampleUkkonen6.png|300px]]
 
|style="background:#ffffff"|[[Файл:ExampleUkkonen6.png|300px]]
 
|}
 
|}
Строка 37: Строка 43:
  
 
{{Определение
 
{{Определение
|definition= Пусть <tex>x\alpha</tex> обозначает произвольную строку, где <tex>x</tex> {{---}} ее первый символ, а <tex>\alpha</tex> {{---}} оставшаяся подстрока(возможно пустая). Если для внутренней вершины <tex>v</tex> с путевой меткой <tex>x\alpha</tex> существует другая вершина <tex>s(v)</tex>  с путевой меткой <tex>\alpha</tex>, то ссылка из <tex>v</tex> в <tex>s(v)</tex> называется '''суффиксной ссылкой'''.}}
+
|definition= Пусть <tex>x\alpha</tex> обозначает произвольную строку, где <tex>x</tex> {{---}} её первый символ, а <tex>\alpha</tex> {{---}} оставшаяся подстрока (возможно пустая). Если для внутренней вершины <tex>v</tex> с путевой меткой <tex>x\alpha</tex> существует другая вершина <tex>s(v)</tex>  с путевой меткой <tex>\alpha</tex>, то ссылка из <tex>v</tex> в <tex>s(v)</tex> называется '''суффиксной ссылкой''' (англ. ''suffix link'').
 
+
}}
 
{{Лемма|id=l3
 
{{Лемма|id=l3
 
|about= Существование суффиксных ссылок
 
|about= Существование суффиксных ссылок
Строка 44: Строка 50:
 
Для любой внутренней вершины <tex>v</tex> суффиксного дерева существует суффиксная ссылка, ведущая в некоторую внутреннюю вершину <tex>u</tex>.
 
Для любой внутренней вершины <tex>v</tex> суффиксного дерева существует суффиксная ссылка, ведущая в некоторую внутреннюю вершину <tex>u</tex>.
 
|proof=
 
|proof=
Рассмотрим внутренную вершину <tex>v</tex> с путевой меткой <tex>t[i..j]</tex>. Так как эта вершина внутренняя, ее путевая метка ветвится справа в исходной строке. Тогда очевидно подстрока <tex>t[i+1..j]</tex> тоже ветвится справа в исходной строке, и ей соответствует некоторая внутренняя вершина <tex>u</tex>. По определению суффиксная ссылка вершины <tex>v </tex> ведет в <tex> u</tex>
+
Рассмотрим внутреннюю вершину <tex>v</tex> с путевой меткой <tex>s[j \ldots i]</tex>. Так как эта вершина внутренняя, её путевая метка ветвится справа в исходной строке. Тогда очевидно подстрока <tex>s[j+1 \ldots i]</tex> тоже ветвится справа в исходной строке, и ей соответствует некоторая внутренняя вершина <tex>u</tex>. По определению суффиксная ссылка вершины <tex>v </tex> ведёт в <tex> u</tex>.
 
}}
 
}}
  
 
=== Использование суффиксных ссылок ===
 
=== Использование суффиксных ссылок ===
[[Файл:ExampleUkkonen7.png|200px|thumb|right|Иллюстрация использования суффиксных ссылок.]]
+
[[Файл:ExampleUkkonen7.png|300px|thumb|right|Использование суффиксных ссылок.]]
Суффиксные ссылки используются для того, чтобы можно было быстро перейти от конца одного суффикса к концу другого, а не спускаться каждый раз от корня. Пусть мы только что продлили суффикс <tex>s[j..i-1]</tex> до суффикса <tex>s[j..i]</tex> и стоим в вершине, в которую ведет ребро с пометкой <tex>t[k+1..r]</tex>, содержащей конец текущего суффикса. Найдем с помощью построенных ссылок конец суффикса <tex>s[j+1..i-1]</tex>. Пройдем вверх по дереву до ближайшей внутренней вершины <tex>v</tex>, в которую ведет ребро с пометкой <tex>t[p..k]</tex>. У вершины <tex>v</tex> есть суффиксная ссылка, так как ссылка для новой внутренней вершины строится внутри фазы ее создания. Пусть суффиксная ссылка ведет в вершину <tex>u</tex>, которой соответствует пометка <tex>t[h..k]</tex> (<tex>h</tex> и <tex>p</tex> могут быть не равны). Теперь пройдем от вершины <tex>u</tex> вниз по дереву к концу суффикса <tex>s[j+1..i-1]</tex>, и сделаем продление до суффикса <tex>s[j+1..i]</tex>.
+
 
 +
Рассмотрим применение суффиксных ссылок. Пусть только что был продлён суффикс <tex>s[j \ldots i-1]</tex> до суффикса <tex>s[j \ldots i]</tex>. Теперь с помощью построенных ссылок можно найти конец суффикса <tex>s[j+1 \ldots i-1]</tex> в суффиксном дереве, чтобы продлить его до суффикса <tex>s[j+1 \ldots i]</tex>. Для этого надо пройти вверх по дереву до ближайшей внутренней вершины <tex>v</tex>, в которую ведёт путь, помеченный <tex>s[j \ldots r]</tex>. У вершины <tex>v</tex> точно есть суффиксная ссылка (о том, как строятся суффиксные ссылки, будет сказано позже, а пока можно просто поверить). Эта суффиксная ссылка ведёт в вершину <tex>u</tex>, которой соответствует путь, помеченный подстрокой <tex>s[j+1 \ldots r]</tex>. Теперь от вершины <tex>u</tex> следует пройти вниз по дереву к концу суффикса <tex>s[j+1 \ldots i-1]</tex> и продлить его до суффикса <tex>s[j+1 \ldots i]</tex>.
 +
 
 +
Подстрока <tex>s[j+1 \ldots i-1]</tex> является суффиксом подстроки <tex>s[j \ldots i-1]</tex>, следовательно после перехода по суффиксной ссылке в вершину, помеченную путевой меткой <tex>s[j+1 \ldots r]</tex>, можно дойти до места, которому соответствует метка <tex>s[r+1 \ldots i-1]</tex>, сравнивая не символы на рёбрах, а лишь длину ребра по первому символу рассматриваемой части подстроки и длину самой этой подстроки. Таким образом можно спускаться вниз сразу на целое ребро.
  
 
=== Построение суффиксных ссылок ===
 
=== Построение суффиксных ссылок ===
  
Заметим что в процессе построения суффиксного дерева уже построенные суффиксные ссылки никак не изменяются. Опишем процесс построения суффиксной ссылки для новой созданной внутренней вершины. Пусть в результате очередного продления была создана новая внутренняя вершина <tex>v</tex> с путевой меткой <tex>t[l..r]</tex>. Не будем специально искать, куда должна указывать ссылка. Перейдем к следующему шагу текущей фазы, на котором в дерево будет добавлен суффикс <tex>s[j+1..i]</tex>. Этот суффикс может так же оканчиваться на ребре, но тогда будет создана новая внутренняя вершина <tex>u</tex>, по определению суффиксная ссылка из вершины <tex>v</tex> ведет в <tex>u</tex>.
+
Легко увидеть, что в процессе построения суффиксного дерева уже построенные суффиксные ссылки никак не изменяются. Поэтому осталось сказать, как построить суффиксные ссылки для созданных вершин. Рассмотрим новую внутреннюю вершину <tex>v</tex>, которая была создана в результате продления суффикса <tex>s[j \ldots i-1]</tex>. Вместо того, чтобы искать, куда должна указывать суффиксная ссылка вершины <tex>v</tex>, поднимаясь от корня дерева для этого, перейдем к продлению следующего суффикса <tex>s[j+1 \ldots i-1]</tex>. И в этот момент можно проставить суффиксную ссылку для вершины <tex> v</tex>. Она будет указывать либо на существующую вершину, если следующий суффикс закончился в ней, либо на новую созданную. То есть суффиксные ссылки будут обновляться с запаздыванием. Внимательно посмотрев на все три правила продления суффиксов, можно осознать, что для вершины <tex> v </tex> точно найдётся на следующей фазе внутренняя вершина, в которую должна вести суффиксная ссылка.
  
 
=== Оценка числа переходов ===
 
=== Оценка числа переходов ===
  
 
{{Определение
 
{{Определение
|definition= '''Глубиной вершины''' <tex>d(v)</tex> назовем число ребер на пути от корня до вершины <tex>v</tex>}}
+
|definition= '''Глубиной вершины''' <tex>d(v)</tex> назовем число рёбер на пути от корня до вершины <tex>v</tex>.}}
  
 
{{Лемма|id=l4
 
{{Лемма|id=l4
Строка 64: Строка 73:
 
При переходе по суффиксной ссылке глубина уменьшается не более чем на <tex>1</tex>.
 
При переходе по суффиксной ссылке глубина уменьшается не более чем на <tex>1</tex>.
 
|proof=  
 
|proof=  
Пусть мы переходим из вершины <tex> v </tex> с путевой меткой <tex>t[i..j]</tex> по суффиксной ссылке в вершину <tex> u </tex> с путевой меткой <tex>t[i+1..j]</tex> Определим множество <tex> A </tex> как множество вершин на пути от корня до <tex> u </tex>, исключая корень. Множество <tex> B </tex> определим как множество вершин на пути от корня до <tex> v </tex>, исключая корень. Если длина первого ребра на пути от корня до <tex> v </tex> равна единице, то выкинем из множества <tex>B</tex> вершину, в которую ведет это ребро. Итого по построению получаем: <tex>|A| = d(u)</tex>, <tex>|B| \ge d(v) - 1</tex>. Теперь заметим, что суффиксная ссылка из любой вершины множества <tex>B</tex> ведет в некоторую вершину множества <tex> A</tex>, и очевидно суффиксные ссылки из разных вершин ведут в разные вершины, поэтому <tex>|A| \ge |B|</tex>, а значит <tex>d(u) \ge d(v) - 1</tex>
+
 
 +
[[Файл:ExampleUkkonen8.png|200px|center|]]
 +
 
 +
Заметим, что на пути <tex>A</tex> в дереве по суффиксу <tex>s[j+1 \ldots i]</tex> не более чем на одну вершину меньше, чем на пути <tex>B</tex> по суффиксу <tex>s[j \ldots i]</tex>. Каждой вершине <tex>v</tex> на пути <tex>B</tex> соответствует вершина <tex>u</tex> на пути <tex>A</tex>, в которую ведёт суффиксная ссылка. Разница в одну вершину возникает, если первому ребру в пути <tex>B</tex> соответсвует метка из одного символа <tex>s_{j}</tex>, тогда суффиксная ссылка из вершины, в которую ведёт это ребро, будет вести в корень.
 
}}
 
}}
  
 
{{Лемма|id=l5
 
{{Лемма|id=l5
 +
|about=о числе переходов внутри фазы
 
|statement=
 
|statement=
Число переходов по ребрам внутри фазы номер <tex>i</tex> не превышает <tex>4i</tex>
+
Число переходов по рёбрам внутри фазы номер <tex>i</tex> равно <tex>O(i)</tex>.
 
|proof=
 
|proof=
Оценим количество переходов по ребрам при поиске конца суффикса. Переход до ближайшей внутренней вершины уменьшает высоту на <tex>1</tex>. Переход по суффиксной ссылке уменьшает высоту не более чем на <tex>1</tex>  (по лемме, доказанной выше). Значит в течение одной фазы вверх мы переходим не более <tex>2i</tex> раз. Но внутри одной фазы начальная глубина не меньше конечной (так как длины суффиксов убывают до <tex>1</tex>), поэтому вниз мы могли пройти не более <tex>2i</tex> ребер. Итого получаем оценку <tex>4i</tex>
+
Оценим количество переходов по рёбрам при поиске конца суффикса. Переход до ближайшей внутренней вершины уменьшает высоту на <tex>1</tex>. Переход по суффиксной ссылке уменьшает высоту не более чем на <tex>1</tex>  (по лемме, доказанной выше). А потом высота увеличивается, пока мы переходим по рёбрам вниз. Так как высота не может увеличиваться больше глубины дерева, а на каждой <tex>j</tex>-ой итерации мы уменьшаем высоту не более, чем на <tex> 2 </tex>, то суммарно высота не может увеличиться больше чем на <tex> 2i</tex>. Итого, число переходов по рёбрам за одну фазу в сумме составляет <tex>O(i)</tex>.
 
}}
 
}}
  
=== Асимтотика алгоритма с использованием суффиксных ссылок ===
+
=== Асимптотика алгоритма с использованием суффиксных ссылок ===
  
Благодаря суффиксным ссылкам количество действий на одной итерации снижается с <tex>O(n^2)</tex> до <tex>O(n)</tex>, так как по доказанной выше лемме на каждом шаге мы делаем не более O(n) переходов. Следовательно, общая асимптотика алгоритма улучшилась до <tex>O(n^2)</tex>.
+
Теперь в начале каждой фазы мы только один раз спускаемся от корня, а дальше используем переходы по суффиксным ссылкам. По доказанной [[#l5 | лемме]] переходов внутри фазы будет <tex>O(i)</tex>. А так как фаза состоит из <tex>i</tex> итераций, то амортизационно получаем, что на одной итерации будет выполнено <tex>O(1)</tex> действий. Следовательно, асимптотика алгоритма улучшилась до <tex>O(n^2)</tex>.
  
 
==Линейный алгоритм==
 
==Линейный алгоритм==
 +
 +
Чтобы улучшить время работы данного алгоритма до <tex>O(n)</tex>, нужно использовать линейное количество памяти, поэтому метка каждого ребра будет храниться как два числа {{---}} позиции её самого левого и самого правого символов в исходном тексте.
  
 
{{Лемма|id=l1
 
{{Лемма|id=l1
Строка 92: Строка 107:
 
|about= Правило 3 заканчивает дело
 
|about= Правило 3 заканчивает дело
 
|statement=
 
|statement=
В любой фазе, если правило продления 3 применяется в продолжении <tex>i</tex>, оно будет реализовываться во всех дальнейших продолжениях (от <tex>i+1</tex> по <tex>j+1</tex>) до конца фазы.  
+
В любой фазе, если правило продления 3 применяется в продолжении суффикса, начинающего в позиции <tex>j</tex>, оно же и будет применяться во всех дальнейших продолжениях (от <tex>j+1</tex> по <tex>i</tex>) до конца фазы.  
<br>
 
 
|proof=
 
|proof=
При использовании правила продолжения 3 путь, помеченный <tex>s[i..j]</tex> в текущем дереве, должен продолжаться символом <tex>j+1</tex>, и точно так же продолжается путь, помеченный <tex>s[i+1..j]</tex>, поэтому правило 3 применяется в продолжениях <tex>i+1, i+2, ..., j+1</tex>
+
При использовании правила продолжения 3 путь, помеченный <tex>s[j \ldots i-1]</tex> в текущем дереве, должен продолжаться символом <tex>i</tex>, и точно так же продолжается путь, помеченный <tex>s[j+1 \ldots i-1]</tex>, поэтому правило 3 применяется в продолжениях <tex>j+1,\ j+2, \ldots, i</tex>.
 
}}
 
}}
  
Когда используется 3-е правило продления суффикса, никакой работы делать не нужно, так как требуемый суффикс уже в дереве есть. Поэтому можно заканчивать текущую итерацию после первого же использования этого правила. Так как лист навсегда останется листом, зададим метку ребра ведущего в этот лист как <tex>t[i..x]</tex>, где <tex>x</tex>{{---}} ссылка на переменную, хранящую конец текущей подстроки. На следующих итерациях к этому ребру может применяться правило ответвления, но при этом будет меняться только левый(начальный) индекс <tex>i</tex>. Таким образом мы сможем удлинять все суффиксы, заканчивающиеся в листах за <tex>O(1)</tex>.  
+
Когда используется 3-е правило продления суффикса, никакой работы делать не нужно, так как требуемый суффикс уже в дереве есть. Поэтому можно заканчивать текущую итерацию после первого же использования этого правила.
 +
 
 +
Так как лист навсегда останется листом, можно задать метку ребра ведущего в этот лист как <tex>s[j \ldots x]</tex>, где <tex>x</tex> {{---}} ссылка на переменную, хранящую конец текущей подстроки. На следующих итерациях к этому ребру может применяться правило ответвления, но при этом будет меняться только левый(начальный) индекс <tex>j</tex>. Таким образом мы сможем удлинять все суффиксы, заканчивающиеся в листах за <tex>O(1)</tex>.
 +
 
 +
Следовательно, на каждой фазе <tex>i</tex> алгоритм реально работает с суффиксами в диапазоне от <tex>j^*</tex> до <tex>k,\ k \leqslant i</tex>, а не от <tex>1</tex> до <tex>i</tex>. Действительно, если суффикс <tex>s[j \ldots i-2]</tex> был продлён до суффикса <tex>s[j \ldots i-1]</tex> на прошлой фазе по правилу 1, то он и дальше будет продлеваться по правилу 1 (о чём говорит [[#l1 | лемма]]). Если он был продлён по правилу 2, то была создана новая листовая вершина, значит, на текущей фазе <tex> i </tex> этот суффикс будет продлён до суффикса <tex>s[j \ldots i]</tex> по листовой вершине. Поэтому после применения правила 3 на суффиксе <tex>s[k \ldots i]</tex> текущую фазу можно завершить, а следующую начать сразу с <tex>j^* = k</tex>.
  
 
=== Итоговая оценка времени работы ===
 
=== Итоговая оценка времени работы ===
  
Все неявные продления листов суммарно можно выполнить за <tex>O(n)</tex> ([[#l1|по первой лемме]]). [[#l2|По второй лемме]] алгоритм делает не более <tex>2n</tex> явных продлений. Таким образом, в течение всех <tex>n</tex> итерация суммарно выполняется не более <tex>O(n)</tex> продлений, следовательно, с использованием всех приведенных эвристик, алгоритм Укконена работает за <tex>O(n)</tex>.
+
В течение работы алгоритма создается не более <tex>O(n)</tex> вершин по [[Сжатое_суффиксное_дерево#Количество_вершин | лемме о размере суффиксного дерева для строки]]. Все суффиксы, которые заканчиваются в листах, благодаря [[#l1|первой лемме]] на каждой итерации мы увеличиваем на текущий символ по умолчанию за <tex>O(1)</tex>. Текущая фаза алгоритма будет продолжаться, пока не будет использовано правило продления 3. Сначала неявно продлятся все листовые суффиксы, а потом по правилам 2.а) и 2.б) будет создано несколько новых внутренних вершин. Так как вершин не может быть создано больше, чем их есть, то амортизационно на каждой фазе будет создано <tex>O(1)</tex> вершин. Так как мы на каждой фазе начинаем добавление суффикса не с корня, а с индекса <tex>j*</tex>, на котором в прошлой фазе было применено правило 3, то используя немного модифицированный вариант [[#l5 | леммы о числе переходов внутри фазы]] нетрудно показать, что суммарное число переходов по рёбрам за все <tex>n</tex> фаз равно <tex>O(n)</tex>.
 +
 
 +
Таким образом, при использовании всех приведённых эвристик алгоритм Укконена работает за <tex>O(n)</tex>.
  
 
== Минусы алгоритма Укконена ==
 
== Минусы алгоритма Укконена ==
Не смотря на то, что данный алгоритм является одном из самых простых алгоритмов для построения суффиксных деревьев и использует online подход, у него есть серьезные недостатки, из-за которых его не часто используют на практике:
+
Несмотря на то, что данный алгоритм является одним из самых простых в понимании алгоритмов для построения суффиксных деревьев и использует online подход, у него есть серьёзные недостатки, из-за которых его нечасто используют на практике:
 +
# Размер суффиксного дерева сильно превосходит входные данные, поэтому при очень больших входных данных алгоритм Укконена сталкивается с проблемой ''memory bottleneck problem''(другое её название ''thrashing'')<ref>[http://dspace.library.uvic.ca:8080/bitstream/handle/1828/2901/ThesisBarsky16july.pdf?sequence=1 Marina Barsky {{---}} Suffix trees for very large inputs.]</ref>.
 +
# Для несложных задач, таких как поиск подстроки, проще и эффективней использовать другие алгоритмы (например поиск подстроки с помощью [[Префикс-функция | префикс-функции]]).
 +
# При внимательном просмотре видно, что на самом деле алгоритм работает за время <tex>O(n \cdot |\Sigma|)</tex>, используя столько же памяти, так как для ответа на запрос о существовании перехода по текущему символу за <tex>O(1)</tex> необходимо хранить линейное количество информации от размера алфавита в каждой вершине. Поэтому, если алфавит очень большой требуется чрезмерный объём памяти. Можно сэкономить на памяти, храня в каждой вершине только те символы, по которым из неё есть переходы, но тогда поиск перехода будет занимать <tex>O(\log |\Sigma|)</tex> времени.
 
# Константное время на одну итерацию {{---}} это амортизированная оценка, в худшем случае одна фаза может выполняться за <tex>O(n)</tex> времени. Например, алгоритм  Дэни Бреслауера и Джузеппе Итальяно<ref>[https://books.google.ru/books?id=sGDXz53FwM4C&lpg=PP11&ots=utJ8jnql5h&dq=Dany%20Breslauer%2C%20Giuseppe%20F.%20Italiano%3A%20Near%20Real-Time%20Suffix%20Tree%20Construction%20via%20the%20Fringe%20Marked%20Ancestor%20Problem.&hl=ru&pg=PA156#v=onepage&q&f=false Dany Breslauer, Giuseppe F. Italiano {{---}} Near Real-Time Suffix Tree Construction via the Fringe Marked Ancestor Problem.]</ref>, хоть и строит дерево за <tex>O(n \log \log n)</tex>, но на одну итерацию в худшем случае тратит <tex>O(\log \log n)</tex> времени.
 
# Константное время на одну итерацию {{---}} это амортизированная оценка, в худшем случае одна фаза может выполняться за <tex>O(n)</tex> времени. Например, алгоритм  Дэни Бреслауера и Джузеппе Итальяно<ref>[https://books.google.ru/books?id=sGDXz53FwM4C&lpg=PP11&ots=utJ8jnql5h&dq=Dany%20Breslauer%2C%20Giuseppe%20F.%20Italiano%3A%20Near%20Real-Time%20Suffix%20Tree%20Construction%20via%20the%20Fringe%20Marked%20Ancestor%20Problem.&hl=ru&pg=PA156#v=onepage&q&f=false Dany Breslauer, Giuseppe F. Italiano {{---}} Near Real-Time Suffix Tree Construction via the Fringe Marked Ancestor Problem.]</ref>, хоть и строит дерево за <tex>O(n \log \log n)</tex>, но на одну итерацию в худшем случае тратит <tex>O(\log \log n)</tex> времени.
# Существенно использует константность размера алфавита. Например, [[Алгоритм_Фарача | алгоритм Фарах-Колтона]] строит суффиксное дерево за линейное время независимо от размера алфавита.
+
# На сегодняшний день существуют кэш-эффективные алгоритмы, превосходящие алгоритм Укконена на современных процессорах<ref>[https://www.google.ru/url?sa=t&rct=j&q=&esrc=s&source=web&cd=6&ved=0CFMQFjAF&url=http%3A%2F%2Fwww.researchgate.net%2Fprofile%2FYuanyuan_Tian%2Fpublication%2F30848628_Practical_methods_for_constructing_suffix_trees%2Flinks%2F0046352b38e5dc849e000000.pdf&ei=Bh4sVZL8EIausAHujoDoBg&usg=AFQjCNEAr63t7zZnWZPKYIZLjQQInbelSg&sig2=jAPs1IULJvJZt8xwx5PYtA&bvm=bv.90491159,d.bGg&cad=rja Yuanyuan Tian, Sandeep Tata, Richard A. Hankins, Jignesh M. Patel {{---}} Practical methods for constructing suffix trees.]</ref>.
# На сегодняшний день существуют кэш-эффективные алгоритмы, которые превосходят алгоритм Укконена на современных процессорах<ref>[https://www.google.ru/url?sa=t&rct=j&q=&esrc=s&source=web&cd=6&ved=0CFMQFjAF&url=http%3A%2F%2Fwww.researchgate.net%2Fprofile%2FYuanyuan_Tian%2Fpublication%2F30848628_Practical_methods_for_constructing_suffix_trees%2Flinks%2F0046352b38e5dc849e000000.pdf&ei=Bh4sVZL8EIausAHujoDoBg&usg=AFQjCNEAr63t7zZnWZPKYIZLjQQInbelSg&sig2=jAPs1IULJvJZt8xwx5PYtA&bvm=bv.90491159,d.bGg&cad=rja Yuanyuan Tian, Sandeep Tata, Richard A. Hankins, Jignesh M. Patel {{---}} Practical methods for constructing suffix trees.]</ref>.
+
# Также алгоритм предполагает, что дерево полностью должно быть загружено в оперативную память. Если же требуется работать с большими размерами данных, то становится не так тривиально модифицировать алгоритм, чтобы он не хранил всё дерево в ней<ref>[http://arxiv.org/pdf/1012.4074.pdf Woong-Kee Loh, Yang-Sae Moon, Wookey Lee {{---}} A fast divide-and-conquer algorithm for indexing human genome sequences.]</ref>.
 
 
== Реализация ==
 
  <font color=green>// <tex>s</tex> {{---}} исходный текст</font>
 
  <font color=green>// <tex>n</tex> {{---}} длина текста</font>
 
  <font color=green>// <tex>t</tex> {{---}} массив, в котором хранится дерево</font>
 
  <font color=green>// <tex>sz</tex> {{---}} размер суффиксного дерева</font>
 
 
  '''struct node'''
 
    <tex>l, r, par,link</tex>
 
    <tex>\mathtt{next}[]</tex>
 
    '''function''' <tex>\mathtt{len}():</tex>
 
      '''return''' <tex>r - l</tex>
 
    '''function''' <tex>\mathtt{get}(c):</tex>
 
      '''if''' <tex>!next.count(c)</tex>
 
        <tex>\mathtt{next}[c] = -1</tex>
 
      '''return''' <tex>\mathtt{next}[c]</tex>
 
 
  '''struct state'''
 
    <tex>v</tex> <font color=green>// номер вершины, в которой мы остановились на предыдущей итерации</font>
 
    <tex>pos</tex> <font color=green>// позиция на метке ребра, ведущего в эту вершину</font>
 
 
 
  '''state''' <tex>ptr(0, 0)</tex> <font color=green>// указатель на конец самого длинного не уникального суффикса</font>
 
 
  '''function''' <tex>\mathtt{go}(st,</tex> <tex>l,</tex> <tex>r):</tex>
 
    '''while''' <tex>l < r</tex>
 
      '''if''' <tex>st.pos == \mathtt{t}[st.v].\mathtt{len}()</tex>
 
        <tex>st = \mathtt{state}(\mathtt{t}[st.v].\mathtt{get}(\mathtt{s}[l]),</tex> <tex>0)</tex>
 
        '''if''' <tex>st.v == -1</tex>
 
          '''return''' <tex>st</tex>
 
      '''else'''
 
        '''if''' <tex>\mathtt{s}[\mathtt{t}[st.v].l + st.pos] \ne \mathtt{s}[l]</tex>
 
          '''return''' <tex>\mathtt{state}(-1, -1)</tex>
 
        '''if''' <tex>r - l < \mathtt{t}[st.v].\mathtt{len}() - st.pos</tex>
 
          '''return''' <tex>\mathtt{state}(st.v,</tex> <tex>st.pos + r - l)</tex>
 
        <tex>l = l + \mathtt{t}[st.v].\mathtt{len}() - st.pos</tex>
 
        <tex>st.pos = \mathtt{t}[st.v].\mathtt{len}()</tex>
 
    '''return''' <tex>st</tex>
 
 
  '''function''' <tex>\mathtt{split}(st):</tex>
 
    '''if''' <tex>st.pos == \mathtt{t}[st.v].\mathtt{len}()</tex>
 
      '''return''' <tex>st.v</tex>
 
    '''if''' <tex>st.pos == 0</tex>
 
      '''return''' <tex>\mathtt{t}[st.v].par</tex>
 
    <tex>\mathtt{node}</tex> <tex>v = \mathtt{t}[st.v]</tex>
 
    <tex>id = sz</tex>
 
    <tex>sz = sz + 1</tex>
 
    <tex>\mathtt{t}[id] = \mathtt{node}(v.l,</tex> <tex>v.l + st.pos,</tex> <tex>v.par)</tex>
 
    <tex>\mathtt{t}[v.par].\mathtt{get}(\mathtt{s}[v.l]) = id</tex>
 
    <tex>\mathtt{t}[id].\mathtt{get}(\mathtt{s}[v.l + st.pos]) = st.v</tex>
 
    <tex>\mathtt{t}[st.v].par = id</tex>
 
    <tex>\mathtt{t}[st.v].l = \mathtt{t}[st.v].l + st.pos</tex>
 
    '''return''' <tex>id</tex>
 
 
 
  '''function''' <tex>\mathtt{getLink}(v):</tex>
 
    '''if''' <tex>\mathtt{t}[v].link \ne -1 </tex>
 
      '''return''' <tex>\mathtt{t}[v].link</tex>
 
    '''if''' <tex>\mathtt{t}[v].par == -1 </tex>
 
      '''return''' <tex>0</tex>
 
    <tex>to = \mathtt{getLink}(\mathtt{t}[v].par)</tex>
 
    '''return''' <tex>\mathtt{t}[v].link=\mathtt{split}(\mathtt{go}(\mathtt{state}(to,</tex> <tex>\mathtt{t}[to].\mathtt{len}()),</tex> <tex>\mathtt{t}[v].l+(\mathtt{t}[v].par==0),</tex> <tex>\mathtt{t}[v].r))</tex>
 
 
  '''funciton''' <tex>\mathtt{treeExtend}(pos):</tex>
 
    '''while''' <tex>true</tex>
 
      <tex>\mathtt{state}</tex> <tex>nptr = \mathtt{go}(ptr,</tex> <tex>pos,</tex> <tex>pos + 1)</tex>
 
      '''if''' <tex>nptr.v \ne -1</tex>
 
        <tex>ptr = nptr</tex>
 
        '''return'''
 
      <tex>mid = \mathtt{split}(ptr)</tex>
 
      <tex>leaf = sz</tex>
 
      <tex>sz = sz + 1</tex>
 
      <tex>\mathtt{t}[leaf] = \mathtt{node}(pos,</tex> <tex>n,</tex> <tex>mid)</tex>
 
      <tex>\mathtt{t}[mid].\mathtt{get}(\mathtt{s}[pos]) = leaf</tex>
 
      <tex>ptr.v = \mathtt{getLink}(mid)</tex>
 
      <tex>ptr.pos = \mathtt{t}[ptr.v].\mathtt{len}()</tex>
 
      '''if''' <tex>!mid</tex>
 
        '''break'''
 
 
  '''function''' <tex>\mathtt{buildTree}():</tex>
 
    <tex>sz = 1</tex>
 
    '''for''' <tex>i = 0...n</tex>
 
      <tex>\mathtt{treeExtend}(i)</tex>
 
  
 
== См. также==
 
== См. также==
 
* [[Алгоритм МакКрейта]]
 
* [[Алгоритм МакКрейта]]
* [[Алгоритм Фарача]]
+
* [[Алгоритм Фарача| Алгоритм Фараx-Колтона]]
 +
* [[Суффиксный бор]]
  
 
==Примечания==
 
==Примечания==
Строка 199: Строка 142:
 
<references />
 
<references />
  
== Источники ==
+
== Источники информации ==
 
* Дэн Гасфилд — Строки, деревья и последовательности в алгоритмах: Информатика и вычислительная биология — СПб.: Невский Диалект; БХВ-Петербург, 2003. — 654 с: ил.
 
* Дэн Гасфилд — Строки, деревья и последовательности в алгоритмах: Информатика и вычислительная биология — СПб.: Невский Диалект; БХВ-Петербург, 2003. — 654 с: ил.
 +
* [http://yury.name/internet/01ianote.pdf Юрий Лифшиц {{---}} Построение суффиксного дерева за линейное время.]
 
* [http://e-maxx.ru/algo/ukkonen MAXimal :: algo :: Суффиксное дерево. Алгоритм Укконена]
 
* [http://e-maxx.ru/algo/ukkonen MAXimal :: algo :: Суффиксное дерево. Алгоритм Укконена]
* [http://habrahabr.ru/post/111675/ Habrahabr Построение суффиксного дерева: алгоритм Укконена]
+
* [http://habrahabr.ru/post/111675/ Habrahabr {{---}} Построение суффиксного дерева: алгоритм Укконена]
  
 
[[Категория: Алгоритмы и структуры данных]]
 
[[Категория: Алгоритмы и структуры данных]]
 
[[Категория: Словарные структуры данных]]
 
[[Категория: Словарные структуры данных]]
 
[[Категория: Суффиксное дерево]]
 
[[Категория: Суффиксное дерево]]

Текущая версия на 19:17, 4 сентября 2022

Алгоритм Укконена (англ. Ukkonen's algorithm) — алгоритм построения суффиксного дерева для заданной строки [math]s[/math] за линейное время.

Алгоритм за O(n3)

Рассмотрим сначала наивный метод, который строит дерево за время [math]O(n^3)[/math], где [math]n[/math] — длина исходной строки [math]s[/math]. В дальнейшем данный алгоритм будет оптимизирован таким образом, что будет достигнута линейная скорость работы.

Определение:
Неявное суффиксное дерево (англ. implicit suffix tree, IST) строки [math]S[/math] — это суффиксное дерево, построенное для строки [math]S[/math] без добавления [math]\$[/math].
Пример построения суффиксного дерева алгоритмом Укконена.

Алгоритм последовательно строит неявные суффиксные деревья для всех префиксов исходного текста [math]S = s_{1}s_{2} \ldots s_{n}[/math]. На [math]i[/math]-ой фазе неявное суффиксное дерево [math]\tau_{i-1}[/math] для префикса [math]s[1 \ldots i-1][/math] достраивается до [math]\tau_{i}[/math] для префикса [math]s[1 \ldots i][/math]. Достраивание происходит следующим образом: для каждого суффикса подстроки [math]s[1 \ldots i-1][/math] необходимо спуститься от корня дерева до конца этого суффикса и дописать символ [math]s_i[/math].

Алгоритм состоит из [math]n[/math] фаз. На каждой фазе происходит продление всех суффиксов текущего префикса строки, что требует [math]O(n^2)[/math] времени. Следовательно, общая асимптотика алгоритма составляет [math]O(n^3)[/math].

Псевдокод алгоритма за O(n3)

 for i = 1 .. n
   for j = 1 .. i
     treeExtend(s[j..i]) // добавление текущего суффикса работает за линейное время

Замечание: на первый взгляд, более логичным подходом кажется добавление всех суффиксов строки в дерево по очереди, получив сразу алгоритм со временем работы [math]O(n^2)[/math]. Однако осуществить улучшение данного алгоритма до линейного времени работы будет намного сложней, хотя именно в этом и заключается суть алгоритма МакКрейта.

Продление суффиксов

Ниже приведены возможные случаи, которые могут возникнуть при добавлении символа [math]s_{i}[/math] ко всем суффиксам префикса [math]s[1 \ldots i-1][/math].

Случай Правило Пример
1. Продление листа Пусть суффикс [math]s[k \ldots i-1][/math] заканчивается в листе. Добавим [math]s_{i}[/math] в конец подстроки, которой помечено ребро, ведущее в этот лист. ExampleUkkonen3.png
2. Ответвление а) Пусть суффикс [math]s[k \ldots i-1][/math] заканчивается в вершине, не являющейся листом, из которой нет пути по символу [math]s_{i}[/math]. Создадим новый лист, в который из текущей вершины ведёт дуга с пометкой [math]s_{i}[/math]. ExampleUkkonen4.png
б) Пусть суффикс [math]s[k \ldots i-1][/math] заканчивается на ребре с меткой [math]s[l \ldots r][/math] в позиции [math]p-1(l \leqslant p \leqslant r)[/math] и [math]s_{p} \ne s_{i}[/math]. Разобьем текущее ребро новой вершиной на [math]s[l \ldots p-1][/math] и [math]s[p \ldots r][/math] и подвесим к ней еще одного ребенка с дугой, помеченной [math]s_{i}[/math]. ExampleUkkonen5.png
3. Ничего не делать Пусть суффикс [math]s[k \ldots i-1][/math] заканчивается в вершине, из которой есть путь по [math]s_{i}[/math]. Тогда ничего делать не надо. ExampleUkkonen6.png

Суффиксные ссылки

Определение:
Пусть [math]x\alpha[/math] обозначает произвольную строку, где [math]x[/math] — её первый символ, а [math]\alpha[/math] — оставшаяся подстрока (возможно пустая). Если для внутренней вершины [math]v[/math] с путевой меткой [math]x\alpha[/math] существует другая вершина [math]s(v)[/math] с путевой меткой [math]\alpha[/math], то ссылка из [math]v[/math] в [math]s(v)[/math] называется суффиксной ссылкой (англ. suffix link).
Лемма (Существование суффиксных ссылок):
Для любой внутренней вершины [math]v[/math] суффиксного дерева существует суффиксная ссылка, ведущая в некоторую внутреннюю вершину [math]u[/math].
Доказательство:
[math]\triangleright[/math]
Рассмотрим внутреннюю вершину [math]v[/math] с путевой меткой [math]s[j \ldots i][/math]. Так как эта вершина внутренняя, её путевая метка ветвится справа в исходной строке. Тогда очевидно подстрока [math]s[j+1 \ldots i][/math] тоже ветвится справа в исходной строке, и ей соответствует некоторая внутренняя вершина [math]u[/math]. По определению суффиксная ссылка вершины [math]v [/math] ведёт в [math] u[/math].
[math]\triangleleft[/math]

Использование суффиксных ссылок

Использование суффиксных ссылок.

Рассмотрим применение суффиксных ссылок. Пусть только что был продлён суффикс [math]s[j \ldots i-1][/math] до суффикса [math]s[j \ldots i][/math]. Теперь с помощью построенных ссылок можно найти конец суффикса [math]s[j+1 \ldots i-1][/math] в суффиксном дереве, чтобы продлить его до суффикса [math]s[j+1 \ldots i][/math]. Для этого надо пройти вверх по дереву до ближайшей внутренней вершины [math]v[/math], в которую ведёт путь, помеченный [math]s[j \ldots r][/math]. У вершины [math]v[/math] точно есть суффиксная ссылка (о том, как строятся суффиксные ссылки, будет сказано позже, а пока можно просто поверить). Эта суффиксная ссылка ведёт в вершину [math]u[/math], которой соответствует путь, помеченный подстрокой [math]s[j+1 \ldots r][/math]. Теперь от вершины [math]u[/math] следует пройти вниз по дереву к концу суффикса [math]s[j+1 \ldots i-1][/math] и продлить его до суффикса [math]s[j+1 \ldots i][/math].

Подстрока [math]s[j+1 \ldots i-1][/math] является суффиксом подстроки [math]s[j \ldots i-1][/math], следовательно после перехода по суффиксной ссылке в вершину, помеченную путевой меткой [math]s[j+1 \ldots r][/math], можно дойти до места, которому соответствует метка [math]s[r+1 \ldots i-1][/math], сравнивая не символы на рёбрах, а лишь длину ребра по первому символу рассматриваемой части подстроки и длину самой этой подстроки. Таким образом можно спускаться вниз сразу на целое ребро.

Построение суффиксных ссылок

Легко увидеть, что в процессе построения суффиксного дерева уже построенные суффиксные ссылки никак не изменяются. Поэтому осталось сказать, как построить суффиксные ссылки для созданных вершин. Рассмотрим новую внутреннюю вершину [math]v[/math], которая была создана в результате продления суффикса [math]s[j \ldots i-1][/math]. Вместо того, чтобы искать, куда должна указывать суффиксная ссылка вершины [math]v[/math], поднимаясь от корня дерева для этого, перейдем к продлению следующего суффикса [math]s[j+1 \ldots i-1][/math]. И в этот момент можно проставить суффиксную ссылку для вершины [math] v[/math]. Она будет указывать либо на существующую вершину, если следующий суффикс закончился в ней, либо на новую созданную. То есть суффиксные ссылки будут обновляться с запаздыванием. Внимательно посмотрев на все три правила продления суффиксов, можно осознать, что для вершины [math] v [/math] точно найдётся на следующей фазе внутренняя вершина, в которую должна вести суффиксная ссылка.

Оценка числа переходов

Определение:
Глубиной вершины [math]d(v)[/math] назовем число рёбер на пути от корня до вершины [math]v[/math].


Лемма:
При переходе по суффиксной ссылке глубина уменьшается не более чем на [math]1[/math].
Доказательство:
[math]\triangleright[/math]
ExampleUkkonen8.png
Заметим, что на пути [math]A[/math] в дереве по суффиксу [math]s[j+1 \ldots i][/math] не более чем на одну вершину меньше, чем на пути [math]B[/math] по суффиксу [math]s[j \ldots i][/math]. Каждой вершине [math]v[/math] на пути [math]B[/math] соответствует вершина [math]u[/math] на пути [math]A[/math], в которую ведёт суффиксная ссылка. Разница в одну вершину возникает, если первому ребру в пути [math]B[/math] соответсвует метка из одного символа [math]s_{j}[/math], тогда суффиксная ссылка из вершины, в которую ведёт это ребро, будет вести в корень.
[math]\triangleleft[/math]
Лемма (о числе переходов внутри фазы):
Число переходов по рёбрам внутри фазы номер [math]i[/math] равно [math]O(i)[/math].
Доказательство:
[math]\triangleright[/math]
Оценим количество переходов по рёбрам при поиске конца суффикса. Переход до ближайшей внутренней вершины уменьшает высоту на [math]1[/math]. Переход по суффиксной ссылке уменьшает высоту не более чем на [math]1[/math] (по лемме, доказанной выше). А потом высота увеличивается, пока мы переходим по рёбрам вниз. Так как высота не может увеличиваться больше глубины дерева, а на каждой [math]j[/math]-ой итерации мы уменьшаем высоту не более, чем на [math] 2 [/math], то суммарно высота не может увеличиться больше чем на [math] 2i[/math]. Итого, число переходов по рёбрам за одну фазу в сумме составляет [math]O(i)[/math].
[math]\triangleleft[/math]

Асимптотика алгоритма с использованием суффиксных ссылок

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

Линейный алгоритм

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

Лемма (Стал листом — листом и останешься):
Если в какой-то момент работы алгоритма Укконена будет создан лист с меткой [math]i[/math] (для суффикса, начинающегося в позиции [math]i[/math] строки [math]S[/math]), он останется листом во всех последовательных деревьях, созданных алгоритмом.
Доказательство:
[math]\triangleright[/math]
Это верно потому, что у алгоритма нет механизма продолжения листового ребра дальше текущего листа. Если есть лист с суффиксом [math]i[/math], правило продолжения 1 будет применяться для продолжения [math]i[/math] на всех последующих фазах.
[math]\triangleleft[/math]
Лемма (Правило 3 заканчивает дело):
В любой фазе, если правило продления 3 применяется в продолжении суффикса, начинающего в позиции [math]j[/math], оно же и будет применяться во всех дальнейших продолжениях (от [math]j+1[/math] по [math]i[/math]) до конца фазы.
Доказательство:
[math]\triangleright[/math]
При использовании правила продолжения 3 путь, помеченный [math]s[j \ldots i-1][/math] в текущем дереве, должен продолжаться символом [math]i[/math], и точно так же продолжается путь, помеченный [math]s[j+1 \ldots i-1][/math], поэтому правило 3 применяется в продолжениях [math]j+1,\ j+2, \ldots, i[/math].
[math]\triangleleft[/math]

Когда используется 3-е правило продления суффикса, никакой работы делать не нужно, так как требуемый суффикс уже в дереве есть. Поэтому можно заканчивать текущую итерацию после первого же использования этого правила.

Так как лист навсегда останется листом, можно задать метку ребра ведущего в этот лист как [math]s[j \ldots x][/math], где [math]x[/math] — ссылка на переменную, хранящую конец текущей подстроки. На следующих итерациях к этому ребру может применяться правило ответвления, но при этом будет меняться только левый(начальный) индекс [math]j[/math]. Таким образом мы сможем удлинять все суффиксы, заканчивающиеся в листах за [math]O(1)[/math].

Следовательно, на каждой фазе [math]i[/math] алгоритм реально работает с суффиксами в диапазоне от [math]j^*[/math] до [math]k,\ k \leqslant i[/math], а не от [math]1[/math] до [math]i[/math]. Действительно, если суффикс [math]s[j \ldots i-2][/math] был продлён до суффикса [math]s[j \ldots i-1][/math] на прошлой фазе по правилу 1, то он и дальше будет продлеваться по правилу 1 (о чём говорит лемма). Если он был продлён по правилу 2, то была создана новая листовая вершина, значит, на текущей фазе [math] i [/math] этот суффикс будет продлён до суффикса [math]s[j \ldots i][/math] по листовой вершине. Поэтому после применения правила 3 на суффиксе [math]s[k \ldots i][/math] текущую фазу можно завершить, а следующую начать сразу с [math]j^* = k[/math].

Итоговая оценка времени работы

В течение работы алгоритма создается не более [math]O(n)[/math] вершин по лемме о размере суффиксного дерева для строки. Все суффиксы, которые заканчиваются в листах, благодаря первой лемме на каждой итерации мы увеличиваем на текущий символ по умолчанию за [math]O(1)[/math]. Текущая фаза алгоритма будет продолжаться, пока не будет использовано правило продления 3. Сначала неявно продлятся все листовые суффиксы, а потом по правилам 2.а) и 2.б) будет создано несколько новых внутренних вершин. Так как вершин не может быть создано больше, чем их есть, то амортизационно на каждой фазе будет создано [math]O(1)[/math] вершин. Так как мы на каждой фазе начинаем добавление суффикса не с корня, а с индекса [math]j*[/math], на котором в прошлой фазе было применено правило 3, то используя немного модифицированный вариант леммы о числе переходов внутри фазы нетрудно показать, что суммарное число переходов по рёбрам за все [math]n[/math] фаз равно [math]O(n)[/math].

Таким образом, при использовании всех приведённых эвристик алгоритм Укконена работает за [math]O(n)[/math].

Минусы алгоритма Укконена

Несмотря на то, что данный алгоритм является одним из самых простых в понимании алгоритмов для построения суффиксных деревьев и использует online подход, у него есть серьёзные недостатки, из-за которых его нечасто используют на практике:

  1. Размер суффиксного дерева сильно превосходит входные данные, поэтому при очень больших входных данных алгоритм Укконена сталкивается с проблемой memory bottleneck problem(другое её название thrashing)[1].
  2. Для несложных задач, таких как поиск подстроки, проще и эффективней использовать другие алгоритмы (например поиск подстроки с помощью префикс-функции).
  3. При внимательном просмотре видно, что на самом деле алгоритм работает за время [math]O(n \cdot |\Sigma|)[/math], используя столько же памяти, так как для ответа на запрос о существовании перехода по текущему символу за [math]O(1)[/math] необходимо хранить линейное количество информации от размера алфавита в каждой вершине. Поэтому, если алфавит очень большой требуется чрезмерный объём памяти. Можно сэкономить на памяти, храня в каждой вершине только те символы, по которым из неё есть переходы, но тогда поиск перехода будет занимать [math]O(\log |\Sigma|)[/math] времени.
  4. Константное время на одну итерацию — это амортизированная оценка, в худшем случае одна фаза может выполняться за [math]O(n)[/math] времени. Например, алгоритм Дэни Бреслауера и Джузеппе Итальяно[2], хоть и строит дерево за [math]O(n \log \log n)[/math], но на одну итерацию в худшем случае тратит [math]O(\log \log n)[/math] времени.
  5. На сегодняшний день существуют кэш-эффективные алгоритмы, превосходящие алгоритм Укконена на современных процессорах[3].
  6. Также алгоритм предполагает, что дерево полностью должно быть загружено в оперативную память. Если же требуется работать с большими размерами данных, то становится не так тривиально модифицировать алгоритм, чтобы он не хранил всё дерево в ней[4].

См. также

Примечания

Источники информации