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