https://github.com/lonelywh1te/ukkonen-algorithm
Implementation of the Ukkonen algorithm
https://github.com/lonelywh1te/ukkonen-algorithm
algorithms-and-data-structures binary-tree
Last synced: 6 months ago
JSON representation
Implementation of the Ukkonen algorithm
- Host: GitHub
- URL: https://github.com/lonelywh1te/ukkonen-algorithm
- Owner: lonelywh1te
- Created: 2022-11-24T05:18:15.000Z (almost 3 years ago)
- Default Branch: master
- Last Pushed: 2023-01-27T13:55:47.000Z (almost 3 years ago)
- Last Synced: 2025-02-16T20:47:35.393Z (9 months ago)
- Topics: algorithms-and-data-structures, binary-tree
- Language: C++
- Homepage:
- Size: 8.89 MB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Алгоритм Укконена
_Берегалов А.С
Дальневосточный федеральный университет
Б9121.09.03.03пикд
2022_
## Содержание
- [Содержание]
- [Глоссарий]
- [Введение]
- [Определение]
- [Построение дерева за линейное время]
- [Строка без повторений]
- [Строка с повторениями]
- [Теория]
- [Правила продления суффиксов]
- [Наивный метод]
- [Суффиксные ссылки]
- [Построение суффиксных ссылок]
- [Использование суффиксных ссылок]
- [Лемма-1]
- [Лемма-2]
- [Асимптотика алгоритма с использованием суффиксных ссылок]
- [Алгоритм O(n)]
- [Итоговая оценка времени работы]
- [Тестирование]
- [Входные данные]
- [Выходные данные]
- [Анализ производительности]
- [Функция построения дерева]
- [Функция поиска подстроки]
## Глоссарий
- Бор (англ. trie, луч, нагруженное дерево) — структура данных для хранения набора строк, представляющая из себя подвешенное дерево с символами на рёбрах.
- Внутренняя вершина — любая вершина дерева, имеющая потомков, и таким образом, не являющаяся листом.
- Корень — самый верхний узел дерева.
- Лист — узел, не имеющий дочерних элементов (детей).
- Неявное суффиксное дерево (англ. implicit suffix tree, IST) строки S — это суффиксное дерево, построенное для строки S без добавления $.
- Подстрока — это часть строки, состоящая из некоторого количества смежных символов исходной строки.
- Суффикс — это подстрока строки S, начинающеяся в позиции i и зканчивающеяся в m - где m это длина строки S.
## Введение
Алгоритм Укконена основывается на понятии суффиксного дерева
> _Суффиксное дерево — бор, содержащий все суффиксы некоторой строки (и только их).
Входные данные для алгоритма – это строка s, состоящая из n символов s[0], s[1], …, s[n-1]._ [[17]](https://ru.wikipedia.org/wiki/%D0%A1%D1%83%D1%84%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE)
Оно выглядит следующим образом:

_Дерево для строки "abcxa"_
Суффиксное дерево — дерево с n листьями, обладающее следующими свойствами:
- каждая внутренняя вершина дерева имеет не меньше двух детей;
- каждое ребро помечено непустой подстрокой строки s;
- никакие два ребра, выходящие из одной вершины, не могут иметь пометок, начинающихся с одного и того же символа;
- дерево должно содержать все суффиксы строки s, причем каждый суффикс заканчивается точно в листе и нигде кроме него.
Число вершин в таком дереве — O(n), поскольку листьев не более чем n + 1. [[5]](https://stackoverflow.com/questions/9452701/ukkonens-suffix-tree-algorithm-in-plain-english/9513423#9513423)
Теперь когда мы знакомы с базовым материалом, мы можем приступить собственно к самому алгоритму.
## Определение
> __Алгоритм Укконена__ (англ. Ukkonen's algorithm) — алгоритм построения суффиксного дерева для заданной строки s за линейное время.
Алгоритм построения суффиксного дерева за линейное время был придуман финским математиком Укконеном (Ukkonen) в 1995 году. [[14]](https://www.hse.ru/data/2013/02/17/1308149995/%D0%9E%D0%B1%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5%20%D0%90%D0%BD%D0%BD%D0%BE%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B5%20%D0%A1%D1%83%D1%84%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D1%8B%D0%B5%20%D0%94%D0%B5%D1%80..%D0%BE%D0%B1%D0%B5%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D0%B8%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8%20%28%D0%94%D1%83%D0%B1%D0%BE%D0%B2%20%D0%9C.%29.pdf)
Алгоритм Укконена строит суффиксное дерево, добавляя в него по одной букве. Текущая позиция в дереве соответствует максимальному неполному суффиксу уже добавленных букв, который уже встечался где-то раньше. [[6]](https://en.wikipedia.org/wiki/Ukkonen%27s_algorithm)
## Построение дерева за линейное время
### Строка без повторений
Самый простой пример для построения дерева - строка без повторений.
> abc
Алгоритм работает пошагово, проходя по строке слева направо. Один шаг на каждый символ строки.
Каждый шаг может включать в себя больше чем одну операцию.
Построение начинается слева и сначала вставляется одиночный символ **a** , создавая ребро из корня к листу и хранится как **[0, #]** - это означает что ребро представляет собой подстроку начинающуюся с позиции 0 и заканчивающуюся на текущем конце. Символ **#** означает индекс за символом (грубо говоря конец символа), то есть на данный момент # = 1;
На данный момент дерево выглядит так:


_Рис.1,2 Дерево для строки "a"_
Был вставлен символ **а**, алгоритм переходит к следующему символу.
**Цель: на каждом шаге вставлять все суффиксы до текущей позиции**
Делается это с помощью расширения существующих ребер и добавлении нового ребра.
Теперь дерево имеет вид:


_Рис.3,4 Дерево для строки "ab"_
>Заметим, что ребро **ab** такое же как и ребро **a** из рисунков выше и хранится как **[0, #]**, так произошло из-за того что **#** теперь равен **2**.
Добавляется символ **с**:


_Рис.5,6 Дерево для строки "abc"_
Так строится дерево, которое не имеет повторяющихся символов.
## Строка с повторениями
>abcabxabz
Так как дерево для строки **abc** было изображено выше, просто продолжим строить его.
Добавляется следующий символ **a** и дерево принимает вид:

_Рис.7 Дерево для строки "abca"_
Теперь # = 4. Все ребра дерева расширились, но добавлять новое ребро не стоит.
Есть ребро, которое начинается на символ **a**.
В таких случаях вместо добавления нового ребра, алгоритм проходит по уже существующему.
Прежде чем сделать это, вводятся еще четыре переменные (в дополнение к #), которые, конечно, существовали всё время, но до сих пор не использовались:
- активная вершина - вершина из которой выходит ребро
- активное ребро - индекс первого символа ребра по которому мы будем спускаться
- активная длина - количество символов, которое мы прошли по ребру (насколько мы спустились по ребру)
- остаток - количество суффиксов которые осталось добавить.
> Как только мы спустились по ребру, наши переменные имеют значения:
> - активная вершина = корень
> - активное ребро = 3 (потому что первый символ в ребре равен символу который мы пытались добавить, а его индекс в свою очередь равен трём.)
> - активная длина = 1 (спустились по ребру на один символ, можно заметить на фото: наша позиция отмечена чертой)
> - остаток = 1
Следующий шаг: добавляется символ **b**.
Переменная # = 5, заметим, что активная позиция была внутри ребра, символ который добавляется уже есть в этом ребре, поэтому спуск по ребру продолжается. При этом остаток становится = 2.
> Cуффикс **a** из предыдущего шага никогда не был вставлен должным образом.
Он остался, и поскольку алгоритм продвинулся на один шаг, он вырос с а до ab.
> И необходимо вставить новое финальное ребро b.

_Рис.8 Дерево для строки "abcab"_
Добавляется следующий символ **x**. Переменная # = 6.
На рисунке выше видно, что спуск по ребру невозможен, так как символа **х** нет.
Поэтому ребро делится и подвешивается новая вершина. Теперь дерево имеет вид:

_Рис.9 Построение дерева для строки "abcabx". Проход по ребру_
Теперь нужно вставить оставшиеся суффиксы. Но прежде чем сделать это, необходимо обновить активную позицию.
После каждого разделения, в случае если активной вершиной является корень, переменные изменятся:
> - активная вершина остается корнем
> - активное ребро увеличивается на один (ищется ребро со след. символом, в данном случае b)
> - активная длина уменьшается на один (т.к один суффикс уже добавлен)
Итак, активная позиция снова внутри ребра, осталось добавить 2 суффикса, спуск дальше невозможен, поэтому ребро снова делится:

_Рис.10 Построение дерева для строки "abcabx". Расширение_
> Если ребро разделяется и вставляется новая вершина, и если это не первая вершина, созданная на текущем шаге, ранее вставленная вершина и новая вершина соединяются через специальный указатель, **суффиксную ссылку**
Второй суффикс добавлен, теперь остаток = 1. Так как активная позиция - корень, то просто добавляется новое ребро **х**. Остаток = 0.

_Рис.11 Дерево для строки "abcabx"_
На данный момент имеется дерево для строки **abcabx**.
Пропустим следующие два шага с добавлением символом a и b, так как на них мы просто спускаемся по ребру и приходим к его концу.
Переменная # равна 7, а остаток = 2:

_Рис.12 Дерево для строки "abcabx", активная вершина 4_
На следующем шаге, переменная # = 8, добавляется символ **z**, так как ребра начинающегося на данный символ нет - создадается новое ребро и осуществляется переход по суффиксной ссылке.
> После разделения или добавления ребра из активной вершины, которая не является корнем, нужно перейти по суффиксной ссылке, выходящей из этой вершины, если таковая имеется. Если суффиксная ссылка отсутствует, активная вершина устанавливается корнем. активное ребро и активная длина остаются без изменений.

_Рис.13 Построение дерева для строки "abcabxabz"_
Так как остаток = 1, в активную вершину добавляется новое ребро с соответствующим символом:

_Рис.13 Построение дерева для строки "abcabxabz". Переход по суффиксной ссылке_
Следующим шагом будет добавление нового ребра **z** и **$**, переменная # = 9:

_Рис.13 Дерево для строки "abcabxabz$"_
> Так как дерево должно содержать все суффиксы строки, причем каждый суффикс заканчивается точно в листе и нигде кроме него, используется специальный символ $.
> Любой суффикс строки с защитным символом действительно заканчивается в листе и только в листе, т. к. в такой строке не существует двух различных подстрок одинаковой длины, заканчивающихся на $.
На этом построение дерева для строки закончено.
## Теория
### Правила продления суффиксов
Пусть _S[j..i]_ = β — суффикс _S[1..i]_. В продолжении j, когда алгоритм находит конец β в текущем дереве, он продолжает β, чтобы обеспечить присутствие суффикса _βS(i + 1)_ в дереве. Алгоритм действует по одному из следующих трех правил.
__Правило 1.__ В текущем дереве путь β кончается в листе. Это значит, что путь от корня с меткой β доходит до конца некоторой «листовой» дуги (дуги, входящей в лист). При изменении дерева нужно добавить к концу метки этой листовой дуги символ _S(i + 1)_.
__Правило 2.__ Ни один путь из конца строки β не начинается символом _S(i + 1)_, но по крайней мере один начинающийся оттуда путь имеется. В этом случае должна быть создана новая листовая дуга, начинающаяся в конце β, помеченная символом _S(i + 1)_. При этом, если β кончается внутри дуги, должна быть создана новая вершина. Листу в конце новой листовой дуги сопоставляется номер j. Таким образом, в правиле 2 возможно два случая.
__Правило 3.__ Некоторый путь из конца строки β начинается символом _S(i + 1)_. В этом случае строка _βS(i + 1)_ уже имеется в текущем дереве, так что ничего не надо делать (в неявном суффиксном дереве конец суффикса не нужно помечать явно). [[2]](https://vk.cc/cj1OgK)
### Наивный метод O(n³)
Алгоритм последовательно строит неявные суффиксные деревья_ для всех префиксов исходного текста _S = s₁s₂…sₙ_.
На i-ой фазе неявное суффиксное дерево tᵢ-₁ для префикса _s[1…i − 1]_ достраивается до tᵢ для префикса _s[1…i]_.
Достраивание происходит следующим образом: для каждого суффикса подстроки _s[1…i − 1]_ необходимо спуститься от корня дерева до конца этого суффикса и дописать символ sᵢ.
Алгоритм состоит из n фаз. На каждой фазе происходит продление всех суффиксов текущего префикса строки, что требует O(n²) времени. Следовательно, общая асимптотика алгоритма составляет O(n³). [[2]](https://vk.cc/cj1OgK)
>__Неявное суффиксное дерево__ (англ. implicit suffix tree, IST) — это суффиксное дерево, построенное для строки S без добавления $.
### Суффиксные ссылки
> Пусть xA обозначает произвольную строку, где x — её первый символ, а A — оставшаяся подстрока (возможно пустая). Если для внутренней вершины _v_ с ребром xА существует другая вершина _s(v)_ с ребром α, то ссылка из _v_ в _s(v)_ называется __суффиксной ссылкой__ (англ. suffix link).
#### Построение суффиксных ссылок
Рассмотрим новую внутреннюю вершину _v_, которая была создана в результате продления суффикса _s[j…i − 1]_.
Вместо того, чтобы искать, куда должна указывать суффиксная ссылка вершины _v_, поднимаясь от корня дерева для этого, перейдем к продлению следующего суффикса _s[j+1…i−1]_. И в этот момент можно проставить суффиксную ссылку для вершины _v_.
Она будет указывать либо на существующую вершину, если следующий суффикс закончился в ней, либо на новую созданную. То есть суффиксные ссылки будут обновляться с запаздыванием. [[1]](https://vk.cc/cj1OgK)
#### Использование суффиксных ссылок
Пусть только что был продлён суффикс _[j…i − 1]_ до суффикса _s[j…i]_.
Теперь с помощью построенных ссылок можно найти конец суффикса _s[j + 1…i − 1]_ в суффиксном дереве, чтобы продлить его до суффикса _s[j + 1…i]_. Для этого надо пройти вверх по дереву до ближайшей внутренней вершины _v_, в которую ведёт путь, помеченный _s[j…r]_. У вершины _v_ точно есть суффиксная ссылка. Эта суффиксная ссылка ведёт в вершину _u_, которой соответствует путь, помеченный подстрокой _s[j + 1…r]_. Теперь от вершины u следует пройти вниз по дереву к концу суффикса _s[j + 1…i − 1]_ и продлить его до суффикса _s[j + 1…i]_.
Подстрока _s[j + 1…i − 1]_ является суффиксом подстроки _s[j…i − 1]_, следовательно после перехода по суффиксной ссылке в вершину, помеченную путевой меткой _s[j + 1…r]_, можно дойти до места, которому соответствует метка _s[r + 1…i − 1]_, сравнивая не символы на рёбрах, а лишь длину ребра по первому символу рассматриваемой части подстроки и длину самой этой подстроки. Таким образом можно спускаться вниз сразу на целое ребро.
#### Первая лемма
При переходе по суффиксной ссылке глубина уменьшается не более чем на 1.
>_Глубиной вершины назовем число рёбер на пути от корня до вершины _v__.
__Доказательство__:
Заметим, что на пути A в дереве по суффиксу _s[j + 1…i]_ не более чем на одну вершину меньше, чем на пути B по суффиксу _s[j…i]_. Каждой вершине _v_ на пути B соответствует вершина _u_ на пути A, в которую ведёт суффиксная ссылка. Разница в одну вершину возникает, если первому ребру в пути B соответсвует метка из одного символа _sⱼ_, тогда суффиксная ссылка из вершины, в которую ведёт это ребро, будет вести в корень.
#### Вторая лемма
Число переходов по рёбрам внутри фазы номер i равно O(i).
__Доказательство__:
Оценим количество переходов по рёбрам при поиске конца суффикса. Переход до ближайшей внутренней вершины уменьшает высоту на 1. Переход по суффиксной ссылке уменьшает высоту не более чем на 1 (по лемме, доказанной выше). А потом высота увеличивается, пока мы переходим по рёбрам вниз. Так как высота не может увеличиваться больше глубины дерева, а на каждой j-ой итерации мы уменьшаем высоту не более, чем на 2, то суммарно высота не может увеличиться больше чем на 2i. Итого, число переходов по рёбрам за одну фазу в сумме составляет O(i).
#### Асимптотика алгоритма с использованием суффиксных ссылок
Теперь в начале каждой фазы мы только один раз спускаемся от корня, а дальше используем переходы по суффиксным ссылкам.
По доказанной лемме переходов внутри фазы будет O(i). А так как фаза состоит из i итераций, то амортизационно получаем, что на одной итерации будет выполнено O(1) действий.
Следовательно, асимптотика алгоритма улучшилась до O(n²).
### Алгоритм за O(n)
Чтобы улучшить время работы данного алгоритма до O(n), нужно использовать линейное количество памяти, поэтому метка каждого ребра будет храниться как два числа — позиции её самого левого и самого правого символов в исходном тексте.
#### Итоговая оценка времени работы
В течение работы алгоритма создается не более O(n) вершин по лемме о размере суффиксного дерева для строки. Все суффиксы, которые заканчиваются в листах, благодаря первой лемме на каждой итерации мы увеличиваем на текущий символ по умолчанию за O(1). Текущая фаза алгоритма будет продолжаться, пока не будет использовано правило продления 3.
Сначала неявно продлятся все листовые суффиксы, а потом по правилу 2 будет создано несколько новых внутренних вершин. Так как вершин не может быть создано больше, чем их есть, то амортизационно на каждой фазе будет создано O(1) вершин. Так как мы на каждой фазе начинаем добавление суффикса не с корня, а с индекса j∗, на котором в прошлой фазе было применено правило 3, то используя немного модифицированный вариант леммы о числе переходов внутри фазы нетрудно показать, что суммарное число переходов по рёбрам за все n фаз равно O(n).
Таким образом, при использовании всех приведённых эвристик алгоритм Укконена работает за O(n). [[2]](https://vk.cc/cj1OgK)
## Тестирование
Тестирование запускается функцией __test_tree()__;
### Входные данные
Формат входных данных:
На вход подается файл - номер_теста
> Строка S (пример: "abcxab")
> Подстрока строки S (пример: "xab")
> Вхождение подстроки в строку S (пример: 3)
Формат выходных данных:
На выходе создается файл - result
### Выходные данные
Тестирование считается успешным, если все тесты были пройдены.
> test #1 - ok
> test #2 - ok
> test #3 - ok
> ...
> test #21 - ok
>
> GENERATED TESTS
> seed: *random_seed*
>
> test #1 - ok
> test #2 - ok
> test #3 - ok
> ...
> test #100 - ok
Если тест был провален, дополнительно выводится строка, подстрока, ответ и вывод программы.
> test #1 - ok
> test #2 - ok
> ...
> test #x - failed
> ㅤㅤstr: abcxab
> ㅤㅤsubstr: xab
> ㅤㅤanswer: 3
> ㅤㅤoutput: 2
В таком случае тестирование прекращается, программа завершается с ошибкой -1;
## Анализ производительности
### Функция построения дерева
Замеры производились для строк длиной до 5000 символов.

### Функция поиска подстроки
В замерах использовались:
__std::string.find()__
__suffix_tree::find()__
Поиск случайной подстроки в строке длиной до 5000.

Поиск подстроки длиной 5 с конца строки.

### Вывод
Данный алгоритм в среднем уступает более быстрым алгоритмам (std::string.find()), поэтому его использование рекомендуется только в исключительных случаях. Как пример, если подстрока находится в конце строки.
## Список литературы
- [1] [Простое суффиксное дерево / Хабр](https://habr.com/ru/post/258121/)
- [2] [Алгоритм Укконена — Викиконспекты](https://vk.cc/cj1OgK)
- [3] [АиСД S03E12. Суффиксное дерево. Алгоритм Укконена](https://www.youtube.com/watch?v=WjzR1eFbAeo&t=1328s&ab_channel=PavelMavrin)
- [4] [Visualization of Ukkonen's Algorithm](http://brenden.github.io/ukkonen-animation/)
- [5] [Ukkonen's suffix tree algorithm in plain English / stackoverflow](https://stackoverflow.com/questions/9452701/ukkonens-suffix-tree-algorithm-in-plain-english/9513423#9513423)
- [6] [Ukkonen's algorithm - Wikipedia](https://en.wikipedia.org/wiki/Ukkonen%27s_algorithm)
- [7] [Suffix Tree Application 1 - Substring Check - GeeksforGeeks](https://www.geeksforgeeks.org/suffix-tree-application-1-substring-check/)
- [8] [Linear Time Construction of Suffix Trees with Ukkonen's Algorithm](https://www.youtube.com/watch?v=OT5CigmVfh0&ab_channel=%E8%9C%BB%E8%9B%89)
- [9] [Suffix Tree-Ukkonen's Algorithm - Coding Ninjas CodeStudio](https://www.codingninjas.com/codestudio/library/suffix-tree-ukkonens-algorithm)
- [10] [Chapter on suffix trees - CMU School of Computer Science](https://www.cs.cmu.edu/afs/cs/project/pscico-guyb/realworld/www/slidesF06/cmuonly/gusfield.pdf)
- [11] [A&DS S03E13. Suffix Tree. Ukkonen's Algorithm](https://www.youtube.com/watch?v=C10HoshM_DA&ab_channel=PavelMavrin)
- [12] [Suffix Tree using Ukkonen's algorithm](https://www.youtube.com/watch?v=aPRqocoBsFQ&ab_channel=TusharRoy-CodingMadeSimple)
- [13] [Суффиксные деревья, алгоритм Укконена. Сжатие данных](https://users.math-cs.spbu.ru/~okhotin/teaching/tcs2_2019/okhotin_tcs2alg_2019_l3.pdf)
- [14] [Обобщенные аннотированные суффиксные деревья](https://www.hse.ru/data/2013/02/17/1308149995/%D0%9E%D0%B1%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5%20%D0%90%D0%BD%D0%BD%D0%BE%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B5%20%D0%A1%D1%83%D1%84%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D1%8B%D0%B5%20%D0%94%D0%B5%D1%80..%D0%BE%D0%B1%D0%B5%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D0%B8%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8%20%28%D0%94%D1%83%D0%B1%D0%BE%D0%B2%20%D0%9C.%29.pdf)
- [15] [Алгоритм Укконена на пальцах](http://www.proteus2001.narod.ru/gen/txt/10/ukk.html)
- [16] [Алгоритм Укконена - frwiki.wiki](https://ru.frwiki.wiki/wiki/Algorithme_d%27Ukkonen)
- [17][Суффиксное дерево - Википедия](https://ru.wikipedia.org/wiki/%D0%A1%D1%83%D1%84%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE)