Алгоритм Ахо-Корасик
Алгоритм Ахо-Корасик
В этом примере рассматривается реализация алгоритма Ахо-Корасика на языке программирования Julia. Алгоритм предназначен для поиска множественных образцов в тексте одновременно.
Теоретическая часть
Алгоритм Ахо–Корасик — это эффективный алгоритм поиска подстрок, предназначенный для нахождения всех вхождений конечного множества шаблонных строк (словаря) во входном тексте. Разработанный Альфредом В. Ахо и Маргарет Дж. Корасик в 1975 году, он широко применяется в задачах, требующих одновременного поиска множества шаблонов, таких как текстовая обработка, биоинформатика и сетевая безопасность.
Описание задачи
Даны текстовая строка длины и множество из шаблонов , суммарная длина которых равна (сумма длин всех шаблонов). Алгоритм Ахо–Корасика находит все вхождения любого шаблона в . Конкретно, он возвращает все пары , такие что шаблон совпадает с подстрокой текста , начинающейся с позиции (т.е. ). Алгоритм особенно эффективен при одновременном поиске нескольких шаблонов, поскольку избегает многократного сканирования текста для каждого шаблона в отдельности.
Формальная постановка задачи:
Вход: текст и множество шаблонов .
Выход: все пары , такие что шаблон встречается в , начиная с позиции .
Применения
Алгоритм Ахо–Корасика используется в следующих областях:
-
системы полнотекстового поиска — для поиска множества ключевых слов в документах;
-
системы обнаружения вторжений — для сопоставления сигнатур вредоносных шаблонов в сетевом трафике;
-
биоинформатика — для поиска множества мотивов в последовательностях ДНК;
-
сжатие данных и обработка естественного языка — для анализа на основе шаблонов.
Основная часть
Поля данных
# Все допустимые символы английского алфавита от 'a' до 'z'
const VALID_CHARS = collect('a':'z')
Структура узла конечного автомата
Определение структуры узла бора / конечного автомата Aho-Corasick
mutable struct Node
son::Vector{Int} # номера дочерних вершин по каждому символу ('a'-'z')
ans::Int # количество совпадений, оканчивающихся здесь
fail::Int # ссылка на «предыдущий» узел с таким же суффиксом
du::Int # число зависимых узлов, которые должны быть обработаны после текущего
idx::Int # уникальный индекс для обнаруженного слова
end
Конструктор по умолчанию: создаём новый узел без дочерних узлов и с начальными значениями
Node() = Node(zeros(Int, length(VALID_CHARS)), 0, 1, 0, 0)
Структура автомата Ахо-Корасика
Определение самого автомата Ахо-Корасика
mutable struct ACAutomaton
tr::Vector{Node} # массив всех узлов дерева префиксного бора / автомата
tot::Int # общее количество использованных/существующих узлов
final_ans::Vector{Int} # результат подсчета количества вхождений каждого шаблона
pidx::Int # счетчик уникальных индексов слов
end
Инициализация автомата с максимум max_nodes узлами и корнем
ACAutomaton(max_nodes) = ACAutomaton([Node() for _ in 1:max_nodes], 1, Int[], 0)
Операции над автоматом
Вставка паттерна в автомат
Функция добавления нового шаблона в автомат и присвоение ему уникального ID
function insert(ac::ACAutomaton, pattern)
u = 1 # начальный узел -- корень
for ch in pattern # проход по каждому символу шаблона
char_code = findfirst(==(ch), VALID_CHARS)
isnothing(char_code) && continue
if ac.tr[u].son[char_code] == 0 # если такого дочернего узла нет
ac.tot += 1 # выделяем новый узел
ac.tr[u].son[char_code] = ac.tot
end
u = ac.tr[u].son[char_code] # переходим к следующему узлу
end
# Присваиваем паттерну уникальный идентификатор, если ещё не назначен
if ac.tr[u].idx == 0
ac.pidx += 1
ac.tr[u].idx = ac.pidx
end
# возвращаем уникальный индекс
return ac.tr[u].idx
end
Построение "суффиксных" ссылок
Функция построения суффиксных переходов (fail links) – необходимы для корректной работы алгоритма AC
function build(ac::ACAutomaton)
q = [ac.tr[begin].son[i] for i in eachindex(VALID_CHARS) if ac.tr[begin].son[i] != 0]
while !isempty(q)
u = q[begin]
q = q[begin+1:end]
for i in eachindex(VALID_CHARS)
son_node_idx = ac.tr[u].son[i]
fail_node_idx = ac.tr[u].fail
if son_node_idx != 0
ac.tr[son_node_idx].fail = ac.tr[fail_node_idx].son[i]
ac.tr[ac.tr[son_node_idx].fail].du += 1
push!(q, son_node_idx)
else
ac.tr[u].son[i] = ac.tr[fail_node_idx].son[i] # делаем жадное продолжение
end
end
end
end
Поиск слов во входном тексте
Функция поиска всех слов в тексте с помощью построенного автомата
function query(ac::ACAutomaton, text)
u = 1
for ch in text # идём по символам текста
char_code = findfirst(==(ch), VALID_CHARS)
isnothing(char_code) && continue
u = ac.tr[u].son[char_code]
ac.tr[u].ans += 1 # увеличиваем счётчик совпадений, заканчивающихся в этой вершине
end
end
Расчёт итоговых результатов
После завершения основного прохода, рассчитываем точные значения совпадений для каждого шаблона, учитывая возможную зависимость между узлами через fail-ссылки
function calculate_final_answers(ac::ACAutomaton)
ac.final_ans = zeros(Int, ac.pidx + 1)
# Начинаем с узлов, не имеющих зависимостей
q = filter(i -> ac.tr[i].du == 0, 1:ac.tot)
while !isempty(q)
u = pop!(q)
idx = ac.tr[u].idx
if idx != 0
ac.final_ans[idx] = ac.tr[u].ans
end
v = ac.tr[u].fail
if v > 1
ac.tr[v].ans += ac.tr[u].ans
ac.tr[v].du -= 1
ac.tr[v].du == 0 && push!(q, v) # если теперь этот узел готов к обработке, добавляем в очередь
end
end
end
Тестирование реализации
Функция, которая автоматически тестирует всю систему
function test_aho_corasick(input, text)
max_nodes, n = 200000 + 6, 5 # ограничение на максимальное количество узлов
ac = ACAutomaton(max_nodes) # создаем новый автомат
ids = zeros(Int, n + 1) # массив хранения идентификаторов каждого шаблона
# Добавляем все шаблоны в автомат
for i in 1:n
pattern = input[i]
ids[i+1] = insert(ac, pattern)
end
build(ac) # построили суффиксные ссылки
query(ac, text) # выполнили поиск
calculate_final_answers(ac) # собрали ответы
# Выводим результаты
for i in 1:n
uniqueid = ids[i]
println("Number of instances of '$(input[i])': " => ac.final_ans[uniqueid+1])
end
end
Запустим тестирование:
# Несколько тестовых шаблонов
input = ["a", "bb", "aa", "abaa", "abaaa"]
text = "abaaabaa" # Текст для анализа
test_aho_corasick(input, text)
Заключение
Мы рассмотрели реализацию алгоритма Ахо-Корасика на языке Julia, который позволяет эффективно находить все вхождения нескольких подстрок в текст за один проход. Эта реализация демонстрирует использование структур данных типа графа (дерево префиксов) вместе со специальными указателями (fail-ссылками) для быстрого перехода между состояниями.
Алгоритм особенно полезен для фильтрации текста, компиляции словарей, сетевых протоколов или любых других задач, где требуется параллельный анализ множества образцов. Реализованный код может быть расширен для более широкого спектра символов и использован в крупных NLP-проектах.
Пример разработан с использованием материалов Rosetta Code