Алгоритм Ахо-Корасик — различия между версиями
|  (→Пример реализации) |  (→Пример реализации) | ||
| Строка 51: | Строка 51: | ||
| '''Структура вершины:''' | '''Структура вершины:''' | ||
|   '''struct''' Node: |   '''struct''' Node: | ||
| − |       '''Node''' son[k]  | + |       '''Node''' son[k]                                 <font color=green>// массив сыновей; k {{---}} это размер алфавита</font> | 
| − |       '''Node''' go[k]  | + |       '''Node''' go[k]                                  <font color=green>// массив переходов (запоминаем переходы в ленивой рекурсии)</font> | 
| − |       '''Node''' parent  | + |       '''Node''' parent                                 <font color=green>// вершина родитель</font> | 
| − |       '''Node''' suffLink  | + |       '''Node''' suffLink                               <font color=green>// суффиксная ссылка (вычисляем в ленивой рекурсии)</font> | 
| − |       '''Node''' up  | + |       '''Node''' up                                     <font color=green>// сжатая суффиксная ссылка</font> | 
| − |       '''char''' charToParent  | + |       '''char''' charToParent                           <font color=green>// символ, ведущий к родителю</font> | 
| − |       '''bool''' isLeaf  | + |       '''bool''' isLeaf                                 <font color=green>// флаг, является ли вершина терминалом</font> | 
| − |       '''vector<int>''' leafPatternNumber  | + |       '''vector<int>''' leafPatternNumber               <font color=green>// номера строк, за которые отвечает терминал</font> | 
| '''Функция, для вычисления суффиксной ссылки:''' | '''Функция, для вычисления суффиксной ссылки:''' | ||
|   '''Node''' getSuffLink('''Node''' v): |   '''Node''' getSuffLink('''Node''' v): | ||
| − |       '''if''' '''not''' v.suffLink  | + |       '''if''' '''not''' v.suffLink                           <font color=green>// если суффиксная ссылка ещё не вычислена</font> | 
|          '''if''' v == root '''or''' v.parent == root |          '''if''' v == root '''or''' v.parent == root | ||
|               v.suffLink = root |               v.suffLink = root | ||
| Строка 71: | Строка 71: | ||
| '''Функция, для вычисления перехода:''' | '''Функция, для вычисления перехода:''' | ||
|   '''Node''' getLink('''Node''' v, '''char''' c):   |   '''Node''' getLink('''Node''' v, '''char''' c):   | ||
| − |      '''if''' '''not''' v.go[c]  | + |      '''if''' '''not''' v.go[c]                               <font color=green>// если переход по символу c ещё не вычислен</font> | 
|           '''if''' v.son[c] |           '''if''' v.son[c] | ||
|               v.go[c] = v.son[c] |               v.go[c] = v.son[c] | ||
| Строка 82: | Строка 82: | ||
| '''Функция, для вычисления сжатой суффиксной ссылки:''' | '''Функция, для вычисления сжатой суффиксной ссылки:''' | ||
|   '''Node''' getUp('''Node''' v): |   '''Node''' getUp('''Node''' v): | ||
| − |       '''if''' '''not''' v.up  | + |       '''if''' '''not''' v.up                                 <font color=green>// если сжатая суффиксная ссылка ещё не вычислена</font> | 
|           '''if''' getSuffLink(v).isLeaf |           '''if''' getSuffLink(v).isLeaf | ||
|               v.up = getSuffLink(v) |               v.up = getSuffLink(v) | ||
| Строка 109: | Строка 109: | ||
| '''Функция, для процессинга текста (поиск, встречается строка или нет):''' | '''Функция, для процессинга текста (поиск, встречается строка или нет):''' | ||
|   '''fun''' processText('''string const&''' t, '''vector<bool>&''' found):   <font color=green>// found - это вектор, длина которого равна количеству строк</font> |   '''fun''' processText('''string const&''' t, '''vector<bool>&''' found):   <font color=green>// found - это вектор, длина которого равна количеству строк</font> | ||
| − |       found.assign(w, ''false'')  | + |       found.assign(w, ''false'')                               <font color=green>// w - количество строк</font> | 
|       '''Node''' cur = root |       '''Node''' cur = root | ||
|       '''for''' i = 0 '''to''' t.length - 1   |       '''for''' i = 0 '''to''' t.length - 1   | ||
Версия 00:35, 29 мая 2016
| Задача: | 
| Дан набор строк в алфавите размера суммарной длины . Необходимо найти для каждой строки все ее вхождения в текст за время и памяти. | 
Содержание
Алгоритм
Шаг 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 not v.suffLink                           // если суффиксная ссылка ещё не вычислена
       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 const& s, int patternNumber):
    Node cur = root
    for i = 0 to s.length - 1
        char c = s[i] - 'a'
        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.push_back(patternNumber)
Функция, для процессинга текста (поиск, встречается строка или нет):
fun processText(string const& t, vector<bool>& found):   // found - это вектор, длина которого равна количеству строк
    found.assign(w, false)                               // w - количество строк
    Node cur = root
    for i = 0 to t.length - 1 
        char c = t[i] - 'a'
        cur = getLink(cur, c)
        for j = 0 to cur.leafPatternNumber.size - 1
            found[cur.leafPatternNumber[j]] = true
        /* В этом месте кода должен выполняться переход по сжатой суффиксной ссылке getUp(cur). Для вершины,
           обнаруженной по ней тоже ставим, что она найдена, затем повторяем для её сжатой суффиксной ссылки
           и так до корня. Хорошо ускорит программу сброс сжатых суффиксных ссылок для посещённых вершин. */
Кроме этих функций требуется инициализация, но она имеет отношение только к кодированию, поэтому здесь приведена не будет.
Оптимизации
Если требуется найти только первое вхождение строки в текст, то существенно ускорить работу алгоритма могут пометки о посещённости узла, т.е. если узел посещён, то не переходить по сжатым суффиксным ссылкам. Вместо хранения пометок можно просто сбрасывать сжатую суффиксную ссылку.
Поиск шаблонов с масками
| Задача: | 
| Пусть  — маска, обозначающая любой одиночный символ. Необходимо найти для каждого заданного шаблона с масками все его вхождения в текст. | 
Например, шаблон , который содержит в себе три маски, встречается на позициях и строки .
Алгоритм поиска
Для того чтобы найти все вхождения в текст заданного шаблона с масками , необходимо обнаружить вхождения в текст всех его безмасочных кусков.
Пусть  — набор подстрок
, разделенных масками, и пусть  — их стартовые позиции в . Например, шаблон  содержит две подстроки без масок  и  и их стартовые позиции соответственно  и . Для алгоритма понадобится массив .  — количество встретившихся в тексте безмасочных подстрок шаблона, который начинается в тексте на позиции . Тогда появление подстроки  в тексте на позиции  будет означать возможное появление шаблона на позиции .
-  Используя алгоритм Ахо-Корасик, находим безмасочные подстроки шаблона : когда находим  в тексте  на позиции , увеличиваем на единицу .
-  Каждое , для которого , является стартовой позицией появления шаблона  в тексте.
Рассмотрим подстроку текста . Равенство  будет означать, что подстроки текста  и так далее будут равны соответственно безмасочным подстрокам шаблона . Остальная часть шаблона является масками, поэтому шаблон входит в текст на позиции .
Поиск подстрок заданного шаблона с помощью алгоритма Ахо-Корасик выполняется за время , где  — суммарная длина подстрок, то есть длина шаблона,  — длина текста,  — количество появлений подстрок шаблона. Далее просто надо пробежаться по массиву  и просмотреть значения ячеек за время .

