Алгоритм Ахо-Корасик — различия между версиями
Строка 128: | Строка 128: | ||
== Поиск шаблонов с масками == | == Поиск шаблонов с масками == | ||
− | Пусть <tex>\ | + | Пусть <tex>\varphi</tex> {{---}} маска, обозначающая любой одиночный символ.<BR> |
− | Например, шаблон <tex>ab\ | + | Например, шаблон <tex>ab\varphi\varphi c\varphi</tex> встречается на позициях <tex>2</tex> и <tex>8</tex> строки |
<tex>xabvccababcax</tex>.<BR> | <tex>xabvccababcax</tex>.<BR> | ||
− | + | Надо обнаружить | |
− | |||
безмасочные куски шаблона <tex>Q</tex>:<BR> | безмасочные куски шаблона <tex>Q</tex>:<BR> | ||
− | Пусть {<tex>Q_1</tex>, ..., <tex>Q_k</tex>} {{---}} набор подстрок | + | Пусть <tex>\{</tex><tex>Q_1</tex>, ..., <tex>Q_k</tex><tex>\}</tex> {{---}} набор подстрок |
<tex>Q</tex>, разделенных масками, и пусть <tex>l_1</tex>, ..., | <tex>Q</tex>, разделенных масками, и пусть <tex>l_1</tex>, ..., | ||
− | <tex>l_k</tex> {{---}} их стартовые позиции в <tex>Q</tex>.<BR> | + | <tex>l_k</tex> {{---}} их стартовые позиции в <tex>Q</tex>, <tex>C[i]</tex> {{---}} количество встретившихся в тексте безмасочных подстрок шаблона, который начинается в тексте на позиции <tex>i</tex>. <BR> |
+ | Алгоритм поиска:<BR> | ||
1. <tex>\forall i = 1 \ldots m: C[i] = 0 </tex>, где <tex>m</tex> {{---}} длина текста. <BR> | 1. <tex>\forall i = 1 \ldots m: C[i] = 0 </tex>, где <tex>m</tex> {{---}} длина текста. <BR> | ||
− | 2. Используя | + | 2. Используя алгоритм Ахо-Корасик, находим подстроки <tex>Q</tex>: когда находим <tex>Q_i</tex> |
− | в тексте T на позиции <tex>j</tex>, увеличиваем на единицу <tex>C[j - l_i + 1]</tex>.<BR> | + | в тексте <tex>T</tex> на позиции <tex>j</tex>, увеличиваем на единицу <tex>C[j - l_i + 1]</tex>.<BR> |
3. Каждое <tex>i</tex>, для которого <tex>C[i] = k</tex>, является | 3. Каждое <tex>i</tex>, для которого <tex>C[i] = k</tex>, является | ||
− | стартовой позицией появления шаблона <tex>Q</tex>. | + | стартовой позицией появления шаблона <tex>Q</tex> в тексте. |
+ | |||
+ | == Время работы алгоритма поиска шаблонов с масками == | ||
+ | Поиск подстрок заданного шаблона с помощью алгоритма Ахо-Корасик выполняется за время <tex>O(m+n+a)</tex>, где <tex>n</tex> {{---}} суммарная длина подстрок, то есть длина шаблона, <tex>m</tex> {{---}} длина текста, <tex>a</tex> {{---}} количество появлений подстрок шаблона. Далее просто надо пробежаться по массиву <tex>C</tex> и просмотреть значения ячеек за время <tex>O (m)</tex>. | ||
== Источники == | == Источники == |
Версия 22:18, 15 июня 2014
Содержание
Задача алгоритма
Найти для каждого образца из заданного множества образцов (размером
) все его вхождения в текст за время , где — суммарная длина образцов, — длина текста, — размер ответа (количество пар). В худшем случае , но на практике он встречается редко.Шаг 1
Строим бор из образцов.
Построение выполняется за время , где — суммарная длина образцов.
Пример построенного бора
Бор для набора образцов {he, she, his, hers}:
Шаг 2
Превращаем бор в автомат.
Узлы бора становятся состояниями автомата; корень — начальное состояние.
Узлы бора, в которых заканчиваются образцы, становятся терминалами.
Для переходов по автомату заведём в узлах несколько функций:
1) — возвращает родителя вершины ;
2) — суффиксная ссылка; здесь — сын по символу ;
3) — функция перехода.
Суффиксная ссылка , если — максимальный суффикс , . Обозначение: — слово, приводящее в вершину в боре.
Функции перехода и суффиксные ссылки можно найти либо алгоритмом обхода в глубину с ленивыми вычислениями, либо с помощью алгоритма обхода в ширину.
Пример автомата Ахо-Корасик
Пунктиром обозначены суффиксные ссылки. Из вершин, для которых они не показаны, суффиксные ссылки идут в корень.
Шаг 3
Построение сжатых суффиксных ссылок.
— сжатая суффиксная ссылка, т.е. ближайшее допускающее состояние (терминал) перехода по суффиксным ссылкам.
Сжатые суффиксные ссылки могут отыскиваться при помощи ленивой рекурсии.
Использование автомата
По очереди просматриваем символы текста. Для очередного символа
Примечание. Если требуется найти только первое вхождение образца в текст, то существенно ускорить работу алгоритма могут пометки о посещённости узла, т.е. если узел посещён, то не переходить по сжатым суффиксным ссылкам. Вместо хранения пометок можно просто сбрасывать сжатую суффиксную ссылку.
Пример реализации
Ниже представлена реализация на C++ некоторых функций (используется ленивая рекурсия).
Структура вершины:
struct Node { Node* son[SZ]; // массив сыновей; SZ - это размер алфавита Node* go[SZ]; // массив переходов (запоминаем переходы в ленивой рекурсии) Node* parent; // вершина родитель Node* suffLink; // суффиксная ссылка (вычисляем в ленивой рекурсии) Node* up; // сжатая суффиксная ссылка char charToParent; // символ, ведущий к родителю bool leaf; // флаг, является ли вершина терминалом std::vector<int> leafPatternNumber; // номера образцов, за которые отвечает терминал };
Функция, для вычисления суффиксной ссылки:
Node* getSuffLink(Node* v) { if (!v->suffLink) { // если суффиксная ссылка ещё не вычислена if (v == root || v->parent == root) { v->suffLink = root; } else { v->suffLink = getGo(getSuffLink(v->parent), v->charToParent); } } return v->suffLink; }
Функция, для вычисления перехода:
Node* getGo(Node* v, char c) { if (!v->go[c]) { // если переход по символу c ещё не вычислен if (v->son[c]) { v->go[c] = v->son[c]; } else { v->go[c] = (v == root) ? root : getGo(getSuffLink(v), c); } } return v->go[c]; }
Функция, для вычисления сжатой суффиксной ссылки:
Node* getUp(Node* v) { if (!v->up) { // если сжатая суффиксная ссылка ещё не вычислена if (getSuffLink(v)->leaf) { v->up = getSuffLink(v); } else if (getSuffLink(v) == root) { v->up = root; } else { v->up = getUp(getSuffLink(v)); } } return v->up; }
Функция, для добавление образца в бор:
void addString(std::string const& s, int patternNumber) { Node* cur = root; for (int i = 0; i < s.length(); ++i) { char c = s[i] - 'a'; if (cur->son[c] == 0) { cur->son[c] = new Node; /* здесь также нужно обнулить указатели на переходы и сыновей */ cur->son[c]->suffLink = 0; cur->son[c]->up = 0; cur->son[c]->parent = cur; cur->son[c]->charToParent = c; cur->son[c]->leaf = false; } cur = cur->son[c]; } cur->leaf = true; cur->leafPatternNumber.push_back(patternNumber); }
Функция, для процессинга текста (поиск, встречается образец или нет):
void processText(std::string const& t, std::vector<bool>& found) { // found - это вектор, длина которого равна количеству образцов found.assign(w, false); // w - количество образцов Node* cur = root; for (int i = 0; i < t.length(); ++i) { char c = t[i] - 'a'; cur = getGo(cur, c); for (int j = 0; j < cur->leafPatternNumber.size(); ++j) { found[cur->leafPatternNumber[j]] = true; } /* В этом месте кода должен выполняться переход по сжатой суффиксной ссылке getUp(cur). Для вершины, обнаруженной по ней тоже ставим, что она найдена, затем повторяем для её сжатой суффиксной ссылки и так до корня. Хорошо ускорит программу сброс сжатых суффиксных ссылок для посещённых вершин. */ } }
Кроме этих функций требуется инициализация, но она имеет отношение только к кодированию, поэтому здесь приведена не будет.
Поиск шаблонов с масками
Пусть
Например, шаблон встречается на позициях и строки
.
Надо обнаружить
безмасочные куски шаблона :
Пусть , ..., — набор подстрок
, разделенных масками, и пусть , ...,
— их стартовые позиции в , — количество встретившихся в тексте безмасочных подстрок шаблона, который начинается в тексте на позиции .
Алгоритм поиска:
1. , где — длина текста.
2. Используя алгоритм Ахо-Корасик, находим подстроки : когда находим
в тексте на позиции , увеличиваем на единицу .
3. Каждое , для которого , является
стартовой позицией появления шаблона в тексте.
Время работы алгоритма поиска шаблонов с масками
Поиск подстрок заданного шаблона с помощью алгоритма Ахо-Корасик выполняется за время
, где — суммарная длина подстрок, то есть длина шаблона, — длина текста, — количество появлений подстрок шаблона. Далее просто надо пробежаться по массиву и просмотреть значения ячеек за время .