Алгоритм кнута-морриса-пратта (кмп)

Введем понятие префикс-функции. Префикс-функция для i-ой позиции — это длина максимального префикса строки, который короче i и который совпадает с суффиксом префикса длины i. Если определение Z-функции не сразило оппонента наповал, то уж этим комбо вам точно удастся поставить его на место :) А на человеческом языке это выглядит так: берем каждый возможный префикс строки и смотрим самое длинное совпадение начала с концом префикса (не учитывая тривиальное совпадение самого с собой). Вот пример для «ababcaba»:

префикс префикс p
a a
ab ab
aba aba
abab abab
ababc ababc
ababca ababca
ababcab ababcab
ababcaba ababcaba

Самое первое значение префикс-функции, очевидно, 0. Пусть мы посчитали префикс-функцию до i-ой позиции включительно. Рассмотрим i+1-ый символ. Если значение префикс-функции в i-й позиции Pi, то значит префикс A[..Pi] совпадает с подстрокой A[i-Pi+1..i]. Если символ A[Pi+1] совпадет с A[i+1], то можем спокойно записать, что Pi+1=Pi+1. Но вот если нет, то значение может быть либо меньше, либо такое же. Конечно, при Pi=0 сильно некуда уменьшаться, так что в этом случае Pi+1=0. Допустим, что Pi>0. Тогда есть в строке префикс A[..Pi], который эквивалентен подстроке A[i-Pi+1..i]. Искомая префикс-функция формируется в пределах этих эквивалентных участков плюс обрабатываемый символ, а значит нам можно забыть о всей строке после префикса и оставить только данный префикс и i+1-ый символ — ситуация будет идентичной.
алгоритм кнута-морриса-пратта (кмп) - student2.ru
Задача на данном шаге свелась к задаче для строки с вырезанной серединкой: A[..Pi]A[i+1], которую можно решать рекурсивно таким же способом (хотя хвостовая рекурсия и не рекурсия вовсе, а цикл). То есть если A[PPi+1] совпадет с A[i+1], то Pi+1=PPi+1, а иначе снова выкидываем из рассмотрения часть строки и т.д. Повторяем процедуру пока не найдем совпадение либо не дойдем до 0.
алгоритм кнута-морриса-пратта (кмп) - student2.ru
Повторение этих операций должно насторожить — казалось бы получается два вложенных цикла. Но это не так. Дело в том, что вложенный цикл длиной в k итераций уменьшает префикс-функцию в i+1-й позиции хотя бы на k-1, а для того, чтобы нарастить префикс-функцию до такого значения, нужно хотя бы k-1 раз успешно сопоставить буквы, обработав k-1 символов. То есть длина цикла соответствует промежутку между выполнением таких циклов и поэтому сложность алгоритма по прежнему линейна по длине обрабатываемой строки. С памятью тут такая-же ситуация, как и с Z-функцией — линейная по длине строки, но есть способ сэкономить. Кроме этого есть удобный факт, что символы обрабатываются последовательно, то есть мы не обязаны обрабатывать всю строку, если первое вхождение мы уже получили.
prefFunction(_pattern.size()), pattern(_pattern)

{

prefFunction[0] = 0;

long long q = 0;

for (size_t i = 1; i < pattern.size(); i++)

{

while (q > 0 && pattern[i] != pattern[q])

q = prefFunction[q - 1];

if (pattern[i] == pattern[q])

q++;

prefFunction[i] = q;

}

}

Алгоритм КМП осуществляет поиск подстроки в строке.
Условия:
Даны образец алгоритм кнута-морриса-пратта (кмп) - student2.ru и строка алгоритм кнута-морриса-пратта (кмп) - student2.ru . Требуется определить индекс, начиная с которого строка алгоритм кнута-морриса-пратта (кмп) - student2.ru содержится в строке алгоритм кнута-морриса-пратта (кмп) - student2.ru . Если алгоритм кнута-морриса-пратта (кмп) - student2.ru не содержится в алгоритм кнута-морриса-пратта (кмп) - student2.ru — вернуть индекс, который не может быть интерпретирован как позиция в строке (например, отрицательное число). При необходимости отслеживать каждое вхождение образца в текст имеет смысл завести дополнительную функцию, вызываемую при каждом обнаружении образца.

Алгоритм:
Рассмотрим сравнение строк на позиции алгоритм кнута-морриса-пратта (кмп) - student2.ru , где образец алгоритм кнута-морриса-пратта (кмп) - student2.ru сопоставляется с частью текста алгоритм кнута-морриса-пратта (кмп) - student2.ru . Предположим, что первое несовпадение произошло между алгоритм кнута-морриса-пратта (кмп) - student2.ru и алгоритм кнута-морриса-пратта (кмп) - student2.ru , где алгоритм кнута-морриса-пратта (кмп) - student2.ru . Тогда алгоритм кнута-морриса-пратта (кмп) - student2.ru и алгоритм кнута-морриса-пратта (кмп) - student2.ru .
При сдвиге вполне можно ожидать, что префикс (начальные символы) образца алгоритм кнута-морриса-пратта (кмп) - student2.ru сойдется с каким-нибудь суффиксом (конечные символы) текста алгоритм кнута-морриса-пратта (кмп) - student2.ru . Длина наиболее длинного префикса, являющегося одновременно суффиксом, есть префикс-функция от строки алгоритм кнута-морриса-пратта (кмп) - student2.ru для индекса алгоритм кнута-морриса-пратта (кмп) - student2.ru .
Это приводит нас к следующему алгоритму: пусть алгоритм кнута-морриса-пратта (кмп) - student2.ru — префикс-функция от строки алгоритм кнута-морриса-пратта (кмп) - student2.ru для индекса алгоритм кнута-морриса-пратта (кмп) - student2.ru . Тогда после сдвига мы можем возобновить сравнения с места алгоритм кнута-морриса-пратта (кмп) - student2.ru и алгоритм кнута-морриса-пратта (кмп) - student2.ru без потери возможного местонахождения образца.

std::vector<long long>FindEntries(const std::string& text) const

{

std::vector<long long> answer;

long long q = 0;

for (size_t i = 0; i < text.size(); i++)

{

while (q > 0 && text[i] != pattern[q])

q = prefFunction[q - 1];

if (text[i] == pattern[q])

q++;

if (q == pattern.size())

{

answer.push_back(i - q + 2);

q = prefFunction[q - 1];

}

}

returnanswer;

}

Время работы

Время работы алгоритма составит алгоритм кнута-морриса-пратта (кмп) - student2.ru . Для доказательства этого нужно заметить, что итоговое количество итераций цикла алгоритм кнута-морриса-пратта (кмп) - student2.ru определяет асимптотику алгоритма. Теперь стоит отметить, что алгоритм кнута-морриса-пратта (кмп) - student2.ru увеличивается на каждом шаге не более чем на единицу, значит максимально возможное значение алгоритм кнута-морриса-пратта (кмп) - student2.ru . Поскольку внутри цикла алгоритм кнута-морриса-пратта (кмп) - student2.ru значение алгоритм кнута-морриса-пратта (кмп) - student2.ru лишь уменьшается, получается, что алгоритм кнута-морриса-пратта (кмп) - student2.ru не может суммарно уменьшиться больше, чем алгоритм кнута-морриса-пратта (кмп) - student2.ru раз. Значит цикл алгоритм кнута-морриса-пратта (кмп) - student2.ru в итоге выполнится не более алгоритм кнута-морриса-пратта (кмп) - student2.ru раз, что дает итоговую оценку времени алгоритма алгоритм кнута-морриса-пратта (кмп) - student2.ru .




Алгоритм Ахо-Корасика.

В задаче точного сопоставления множеств (exact set matching problem) требуется обнаружить все появления шаблонов из множества P = {P1,...,Pk} в тексте T[1..m].
Пусть n = Σi=1k |Pi|.
Задача точного сопоставления множеств может быть решена за время
O (|P1| + m + ... + |Pk| + m) = O (n + km),
если применить k раз любой алгоритм, работающий за линейное время.
Алгоритм Ахо-Корасик (АК) (Aho-Corasick algorithm) (AC) - классическое решение задачи точного сопоставления множеств. Он работает за время O (n + m + z) , где z - количество появлений шаблонов в T.
АК основан на структуре данных "дерево ключевых слов" (keyword tree).

Дерево ключевых слов (бор)

Дерево ключевых слов (или "бор") (keyword tree, trie) для множества шаблонов P - это дерево с корнем K, такое что:
1. Каждое ребро e в K отмечено одним символом.
2. Всякие два ребра, исходящие из одной вершины, имеют разные метки.
Определим метку вершины v как конкатенацию меток ребер, составляющих путь из корня в v, и обозначим ее L (v).
3. Для каждого шаблона Pi из множества P есть вершина v, такая что L (v) = Pi.
4. Метка каждой вершины-листа является шаблоном из множества P.

Пример дерева ключевых слов (бора)

Дерево ключевых слов для P = {he, she, his, hers}:
алгоритм кнута-морриса-пратта (кмп) - student2.ru
Бор - хороший способ хранения словаря строк.

Построение бора

Начинаем с дерева из одной вершины (корня); добавляем шаблоны Pi один за другим:
Следуем из корня по ребрам, отмеченным буквами из Pi, пока возможно.
Если Pi заканчивается в v, сохраняем идентификатор Pi (например, i) в v.
Если ребра, отмеченного очередной буквой Pi нет, то создаем новые ребра и вершины для всех оставшихся символов Pi.
Это занимает, очевидно, O (|P1| + ... + |Pk|) = O (n) времени.

Поиск строки в бору

Поиск строки S в бору: начинаем в корне, идем по ребрам, отмеченным символами S, пока возможно.
Если с последним символом S мы приходим в вершину с сохраненным идентификатором, то S - слово из словаря.
Если в какой-то момент ребра, отмеченного нужным символом, не находится, то строки S в словаре нет.
Ясно, что это занимает O (|S|) времени. Таким образом, бор - это эффективный способ хранить словарь и искать в нем слова.
Теперь перейдем от бора к автомату (automaton), чтобы добиться поиска шаблонов в тексте за линейное время.

Автомат Ахо-Корасик

Состояния: узлы бора.
Начальное состояние: корень, обозначим его 0.
Действия автомата определяются тремя функциями, определенными для всех состояний:
1. Функция goto g (s, a) указывает, в какое состояние переходить из данного состояния s при просмотре символа a.
Если ребро (u, v) отмечено символом a, то g (u, a) = v;
g (0, a) = 0 для всех символов a, которыми не отмечено ни одно ребро, выходящее из корня.
~>Автомат остается в корне, пока просматриваются побочные символы.
При всех остальных аргументах g пусть выдает -1.
2. Функция неудачи f (s) указывает, в какое состояние переходить при просмотре неподходящего символа.
Рассмотрим метку вершины s и найдем самый длинный суффикс этой метки, такой, что с него начинается некоторый шаблон из множества P. Тогда f (s) пусть указывает на вершину, метка которой - этот суффикс.
3. Выходная функция out (s) выдает множество шаблонов, которые обнаруживаются при переходе в состояние s.

Пример автомата АК

алгоритм кнута-морриса-пратта (кмп) - student2.ru
Пунктиром обозначены переходы при неудаче (значения функции f); те, которые не показаны, ведут в корень.

Наши рекомендации