Курс "Алгоритмы на последовательностях молекул", весна 2004.
Лекция 4: Сопоставление множеств и алгоритм Ахо-Корасик.
Изложение: Пекка Килпеляйнен, Университет Куопио, Факультет Информатики.
Перевод с английского: Михаил Дворкин, Школа Стайвесант, Нью-Йорк.
Сопоставление множеств и алгоритм Ахо-Корасик*
* Маргарет Корасик - женщина, ее фамилию склонять не следует.
Название "Алгоритм Ахо-Корасика" неверно.
Задача точного сопоставления множеств
В задаче точного сопоставления множеств
(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}:
Бор - хороший способ хранения словаря строк.
Построение бора
Начинаем с дерева из одной вершины (корня); добавляем шаблоны 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.
Пример автомата АК
Пунктиром обозначены переходы при неудаче (значения функции f);
те, которые не показаны, ведут в корень.
Поиск шаблонов с помощью автомата АК
q := 0; // начальное состояние - корень.
for i := 1 to m do
while g (q, T [i]) = -1
do
q := f (q);
q := g (q, T
[i]);
if out (q) ≠ 0 then print i, out
(q);
endfor;
Пример:
Поищите шаблоны в тексте "ushers" с помощью приведенного выше автомата.
Эффективность поиска АК
Теорема. Поиск шаблонов в тексте T [1..m] с помощью автомата АК занимает
O (m + z) времени, где z - количество появлений шаблонов.
Доказательство. При просмотре каждого символа мы совершаем один переход
по функции goto и, возможно, несколько переходов по функции неудачи.
После каждого goto мы либо остаемся в корне, либо переходим в
очередное состояние, глубина которого на 1 больше глубины предыдущего
состояния.
~>Глубина состояния увеличивается не более m раз.
Каждый переход по функции неудачи приближает нас к корню (длина метки вершины заведомо
уменьшается), значит, количество таких переходов не может превосходить m.
z появлений шаблонов могут быть отслежены за суммарное время O (z) (если о
каждом из них сообщать за константное время, например, сохраняя его номер и
позицию в тексте).
Построение автомата АК
Автомат АК строится в два этапа.
Этап I:
1. Строим бор для словаря P.
При добавлении каждого слова Pi из P, вершине v с меткой
Pi, сопоставим out (v) := {Pi}
2. Закончим построение функции goto, добавив несуществующие переходы
из корня: g (0, a) = 0 для всех символов a, не отмечающих ни одного ребра,
выходящего из корня. Это также можно сделать неявно.
Если алфавит фиксирован, этап I занимает O (n) времени.
Результат этапа I:
Этап II:
Q := пустая очередь
для каждого символа a, отмечающего хоть одно ребро из корня
[g (0, a) ≠ 0]
f (g (0, a)) := 0
добавить в очередь Q вершину
g (0, a)
пока очередь Q не пуста
взять вершину r из очереди Q
для каждого символа a, отмечающего хоть одно
ребро из вершины r [g (r, a) ≠ -1]
u := g (r,
a)
добавить в очередь Q
вершину u
v := f
(r)
пока g (v,
a) = -1
v :=
f (v)
f (u) := g (v,
a)
out (u) :=
out (u)∪out (f (u))
Как же это работает?
Функция неудачи и выходная функция вычисляются для всех вершин в порядке
обхода в ширину.
~>Когда мы работаем с вершиной, все вершины, находящиеся ближе, чем она, к
корню (в т. ч. все те, метки которых короче, чем метка данной), уже
обработаны.
Рассмотрим узлы r и u = g (r, a), т. е.
r - родитель u, и L (u) = L (r) a.
Теперь нужно, чтобы f (u) указывало на ту вершину, метка которой
является самым длинным суффиксом L (u), являющимся также началом
некоторого шаблона из множества P. Эта вершина ищется путем просматривания
вершин, метки которых являются все более и более короткими суффиксами L
(r), пока не находится вершина v, для которой g (v,
a) определено; тогда g (v, a) и присваивается
f (u).
Кстати, и v, и g (v, a) могут быть корнем.
Теперь разберемся с out (u) := out (u)∪out (f
(u)).
Это делается потому, что все шаблоны, распознаваемые при переходе в состояние
f (u) (и только они) являются надлежащими суффиксами L
(u) и должны быть отслежены при переходе в состояние u.
Время работы этапа II
Этап II тоже может быть выполнен за время O (n):
Обход в ширину сам по себе занимает время пропорциональное размеру дерева,
т. е. O (n).
Сколько же времени требуется на переходы по функции f в самом
внутреннем цикле?
Рассмотрим вершины u1, ..., ul, проходимые
при введении в бор шаблона a1, ..., al, и
глубины вершин, на которые указывают их функции неудачи, обозначенные
df(u1), ..., df(ul).
При этом df(ui+1) ≤
df(ui) + 1. Это значит, что значения df могут
увеличиваться не более l раз за весь путь. С другой стороны каждое
выполнение v := f (v) уменьшает значение
df(u) как минимум на 1.
~>Итого при просчете функций f для вершин шаблона длины l
совершается не более l переходов.
~>Для всех вершин будет совершено не более n переходов.
А много ли времени нужно на выполнение out (u) :=
out (u)∪out (f (u))?
Нет: множества обнаруживаемых шаблонов можно хранить в виде связных списков,
так что операция объединения выполняется за константное время.
(Все шаблоны в out (f (u)) короче, чем L (u), которая
(возможно) является единственным членом out (u) перед объединением).
Поиск шаблонов с масками
Пусть φ - маска, обозначающая любой одиночный символ.
Например, шаблон abφφcφ встречается на позициях 2 и 8 строки
xabvccababcax.
Если количество φ ограничено сверху константой, то шаблоны с
масками могут быть выявлены за линейное время. Для этого надо обнаружить
безмасочные куски шаблона Q:
Пусть {Q1, ..., Qk} - набор подстрок
Q, разделенных масками, и пусть l1, ...,
lk - их стартовые позиции в Q.
1. for i := 1 to m do C [i] := 0;
2. Используя АК, находим подстроки Q: когда находим Qi
в тексте T на позиции j, увеличиваем на единицу C [j -
li + 1].
3. Каждое i, для которого C [i] = k, является
стартовой позицией появления шаблона Q.
Сложность поиска шаблонов с масками алгоритмом АК
Предобработка: O (m + n)
(Σi=1k|Qi| ≤
|Q| = n)
Поиск: O (m + z), где z - количество появлений.
Каждое появление подстроки Q увеличивает значение одной ячейки C
на 1, и значение каждой ячейки может увеличиваться до k раз.
~>Может быть не более km появлений.
( = O (m), если k ограничено константой.)
Итого, если количество φ ограничено константой, поиск шаблонов с
масками выполняется за время O (n + m).
Сайт создан в системе
uCoz