Алгоритм Ахо-Корасик
| Задача: |
| Дан набор строк в алфавите размера суммарной длины . Необходимо найти для каждой строки все ее вхождения в текст за время и памяти. |
Содержание
Алгоритм
Шаг 1. Построение бора
Строим бор из строк.
Построение выполняется за время , где — суммарная длина строк.
Пример построенного бора
Шаг 2. Преобразование бора
Обозначим за слово, приводящее в вершину в боре.
Узлы бора можно понимать как состояния автомата, а корень как начальное состояние.
Узлы бора, в которых заканчиваются строки, становятся терминальными.
Для переходов по автомату заведём в узлах несколько функций:
- — возвращает родителя вершины ;
- — суффиксная ссылка, и существует переход из в по символу ;
- — функция перехода.
Мы можем понимать рёбра бора как переходы в автомате по соответствующей букве. Однако одними только рёбрами бора нельзя ограничиваться. Если мы пытаемся выполнить переход по какой-либо букве, а соответствующего ребра в боре нет, то мы тем не менее должны перейти в какое-то состояние. Для этого нам и нужны суффиксные ссылки.
Суффиксная ссылка , если — максимальный суффикс , .
Функции перехода и суффиксные ссылки можно найти либо алгоритмом обхода в глубину с ленивыми вычислениями, либо с помощью алгоритма обхода в ширину.
Пример автомата Ахо-Корасик

Пунктиром обозначены суффиксные ссылки. Из вершин, для которых они не показаны, суффиксные ссылки идут в корень.
Суффиксная ссылка для каждой вершины — это вершина, в которой оканчивается наидлиннейший собственный суффикс строки, соответствующей вершине . Единственный особый случай — корень бора; для удобства суффиксную ссылку из него проведём в себя же. Например, для вершины с соответствующей ей строкой максимальным суффиксом является строка , но такой строки больше в нашем автомате нет. Возьмем второй по длине суффикс . Видим, что такая строка заканчивается в вершине . Следовательно суффиксной ссылкой вершины для является вершина .
Шаг 3. Построение сжатых суффиксных ссылок
— сжатая суффиксная ссылка, т.е. ближайшее допускающее состояние (терминал) перехода по суффиксным ссылкам.
Сжатые суффиксные ссылки могут отыскиваться при помощи ленивой рекурсии.
Использование автомата
Теперь нужно сказать немного слов о том, как мы будем использовать наш автомат. По очереди просматриваем символы текста. Для очередного символа переходим из текущего состояния в состояние, которое вернёт функция . Оказавшись в новом состоянии, отмечаем по сжатым суффиксным ссылкам строки, которые нам встретились и их позицию (если требуется). Если новое состояние является терминалом, то соответствующие ему строки тоже отмечаем.
Пример реализации
Ниже представлена реализация некоторых функций (используется ленивая рекурсия).
Структура вершины:
struct Node:
Node son[k] // массив сыновей; 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 not v.go[c] // если переход по символу 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 not v.up // если сжатая суффиксная ссылка ещё не вычислена
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). Для вершины,
обнаруженной по ней тоже ставим, что она найдена, затем повторяем для её сжатой суффиксной ссылки
и так до корня. */
Кроме этих функций требуется инициализация, но она имеет отношение только к кодированию, поэтому здесь приведена не будет.
Оптимизации
- Сброс сжатых суффиксных ссылок для посещённых вершин. Если требуется найти только первое вхождение строки в текст, то существенно ускорить работу алгоритма могут пометки о посещённости узла, т.е. если узел посещён, то не переходить по сжатым суффиксным ссылкам. Вместо хранения пометок можно просто сбрасывать сжатую суффиксную ссылку.
- Сброс пометки терминальной вершины.
Поиск шаблонов с масками
| Задача: |
| Пусть — маска, обозначающая любой одиночный символ. Необходимо найти для каждого заданного шаблона с масками все его вхождения в текст. |
Например, шаблон , который содержит в себе три маски, встречается на позициях и строки .
Алгоритм поиска
Для того чтобы найти все вхождения в текст заданного шаблона с масками , необходимо обнаружить вхождения в текст всех его безмасочных кусков.
Пусть — набор подстрок
, разделенных масками, и пусть — их стартовые позиции в . Например, шаблон содержит две подстроки без масок и и их стартовые позиции соответственно и . Для алгоритма понадобится массив . — количество встретившихся в тексте безмасочных подстрок шаблона, который начинается в тексте на позиции . Тогда появление подстроки в тексте на позиции будет означать возможное появление шаблона на позиции .
- Используя алгоритм Ахо-Корасик, находим безмасочные подстроки шаблона : когда находим в тексте на позиции , увеличиваем на единицу .
- Каждое , для которого , является стартовой позицией появления шаблона в тексте.
Рассмотрим подстроку текста . Равенство будет означать, что подстроки текста и так далее будут равны соответственно безмасочным подстрокам шаблона . Остальная часть шаблона является масками, поэтому шаблон входит в текст на позиции .
Поиск подстрок заданного шаблона с помощью алгоритма Ахо-Корасик выполняется за время , где — суммарная длина подстрок, то есть длина шаблона, — длина текста, — количество появлений подстрок шаблона. Далее просто надо пробежаться по массиву и просмотреть значения ячеек за время .
