Алгоритм Ахо-Корасик
Задача: |
Дан набор строк в алфавите размера | суммарной длины . Необходимо найти для каждой строки все ее вхождения в текст.
Содержание
Алгоритм
Шаг 1. Построение бора
Строим бор из строк.
Построение выполняется за время , где — суммарная длина строк.
Пример построенного бора
:Шаг 2. Преобразование бора
Обозначим за
Узлы бора можно понимать как состояния автомата, а корень как начальное состояние.
Узлы бора, в которых заканчиваются строки, становятся терминальными.
Для переходов по автомату заведём в узлах несколько функций:
- — функция перехода.
Мы можем понимать рёбра бора как переходы в автомате по соответствующей букве. Однако одними только рёбрами бора нельзя ограничиваться. Если мы пытаемся выполнить переход по какой-либо букве, а соответствующего ребра в боре нет, то мы тем не менее должны перейти в какое-то состояние. Для этого нам и нужны суффиксные ссылки.
Суффиксная ссылка , если — максимальный суффикс , .
Функции перехода и суффиксные ссылки можно найти либо алгоритмом обхода в глубину с ленивыми вычислениями, либо с помощью алгоритма обхода в ширину.
Из определений выше можно заметить два следующих факта:
- функция перехода определена через суффиксную ссылку, а суффиксная ссылка — через функию переходов;
- для построения суффиксных ссылок небходимо знать информацию только выше по бору от текущей вершины до корня.
Это позволяет реализовать функции поиска переходов по символу и суффиксных ссылок ленивым образом при помощи взаимной рекурсии.
Пример автомата Ахо-Корасик
Пунктиром обозначены суффиксные ссылки. Из вершин, для которых они не показаны, суффиксные ссылки идут в корень.
Суффиксная ссылка для каждой вершины
— это вершина, в которой оканчивается наидлиннейший собственный суффикс строки, соответствующей вершине . Единственный особый случай — корень бора: для удобства суффиксную ссылку из него проведём в себя же. Например, для вершины с соответствующей ей строкой максимальным подходящим суффиксом является строка . Видим, что такая строка заканчивается в вершине . Следовательно суффиксной ссылкой вершины для является вершина .Шаг 3. Построение сжатых суффиксных ссылок
При построении автомата может возникнуть такая ситуация, что ветвление есть не на каждом символе. Тогда можно маленький бамбук заменить одним ребром. Для этого и используются сжатые суффиксные ссылки.
Сжатые суффиксные ссылки могут отыскиваться при помощи ленивой рекурсии.
Использование автомата
Теперь нужно сказать немного слов о том, как мы будем использовать наш автомат. По очереди просматриваем символы текста. Для очередного символа
Пример реализации
Ниже представлена реализация некоторых функций (используется ленивая рекурсия).
— это размер алфавита.
Структура вершины:
struct Node: Node son[k] // массив сыновей Node go[k] // массив переходов (запоминаем переходы в ленивой рекурсии), используемый для вычисления суффиксных ссылок Node parent // вершина родитель Node suffLink // суффиксная ссылка (вычисляем в ленивой рекурсии) Node up // сжатая суффиксная ссылка char charToParent // символ, ведущий к родителю bool isLeaf // флаг, является ли вершина терминалом vector<int> leafPatternNumber // номера строк, за которые отвечает терминал
Функция, для вычисления суффиксной ссылки:
Node getSuffLink(Node v): if v.suffLink == null // если суффиксная ссылка ещё не вычислена if v == root or v.parent == root v.suffLink = root else v.suffLink = getLink(getSuffLink(v.parent), v.charToParent) return v.suffLink
Функция, для вычисления перехода:
Node getLink(Node v, char c): if v.go[c] == null // если переход по символу c ещё не вычислен if v.son[c] v.go[c] = v.son[c] else if v == root v.go[c] = root else v.go[c] = getLink(getSuffLink(v), c) return v.go[c]
Функция, для вычисления сжатой суффиксной ссылки:
Node getUp(Node v): if v.up == null // если сжатая суффиксная ссылка ещё не вычислена if getSuffLink(v).isLeaf v.up = getSuffLink(v) else if getSuffLink(v) == root v.up = root else v.up = getUp(getSuffLink(v)) return v.up
Функция, для добавление строки в бор:
fun addString(string s, int patternNumber): Node cur = root for i = 0 to s.length - 1 char c = s[i] if cur.son[c] == 0 cur.son[c] = Node /* здесь также нужно обнулить указатели на переходы и сыновей */ cur.son[c].suffLink = 0 cur.son[c].up = 0 cur.son[c].parent = cur cur.son[c].charToParent = c cur.son[c].isLeaf = false cur = cur.son[c] cur.isLeaf = true cur.leafPatternNumber.pushBack(patternNumber)
Функция, для процессинга текста (поиск, встречается строка или нет):
fun processText(string t): Node cur = root for i = 0 to t.length - 1 char c = t[i] - 'a' cur = getLink(cur, c) /* В этом месте кода должен выполняться переход по сжатой суффиксной ссылке getUp(cur). Для вершины, обнаруженной по ней тоже ставим, что она найдена, затем повторяем для её сжатой суффиксной ссылки и так до корня. */
Кроме этих функций требуется инициализация, но она имеет отношение только к кодированию, поэтому здесь приведена не будет.
Оптимизации
Существует несколько оптимизаций данного алгоритма:
- Сброс сжатых суффиксных ссылок для посещённых вершин. Если требуется найти только первое вхождение строки в текст, то существенно ускорить работу алгоритма могут пометки о посещённости узла, т.е. если узел посещён, то не переходить по сжатым суффиксным ссылкам. Вместо хранения пометок можно просто сбрасывать сжатую суффиксную ссылку.
- Сброс пометки терминальной вершины. В изначальном множестве образцов могут быть дублирующиеся строки. Если же таких строк будет много, то наш алгоритм будет очень долго работать. Если же мы будем делать сброс пометки терминальной вершины, когда обнаруживаем ее в первый раз, то таким образом ускорим время работы, так как нас будет интересовать только первое вхождение.
Поиск шаблонов с масками
Задача: |
Пусть | — маска, обозначающая любой одиночный символ. Необходимо найти для каждого заданного шаблона с масками все его вхождения в текст.
Например, шаблон
, который содержит в себе три маски, встречается на позициях и строки .Алгоритм поиска
Для того чтобы найти все вхождения в текст заданного шаблона с масками
Пусть — набор подстрок
, разделенных масками, и пусть — их стартовые позиции в . Например, шаблон содержит две подстроки без масок и и их стартовые позиции соответственно и . Для алгоритма понадобится массив . — количество встретившихся в тексте безмасочных подстрок шаблона, который начинается в тексте на позиции . Тогда появление подстроки в тексте на позиции будет означать возможное появление шаблона на позиции .
- Используя алгоритм Ахо-Корасик, находим безмасочные подстроки шаблона
- Каждое
Рассмотрим подстроку текста
Поиск подстрок заданного шаблона с помощью алгоритма Ахо-Корасик выполняется за время , где — суммарная длина подстрок, то есть длина шаблона, — длина текста, — количество появлений подстрок шаблона. Далее просто надо пробежаться по массиву и просмотреть значения ячеек за время .