Сообщество Engee

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

Автор
avatar-maximsidorovmaximsidorov
Notebook

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

В этом примере рассматривается реализация алгоритма Ахо-Корасика на языке программирования Julia. Алгоритм предназначен для поиска множественных образцов в тексте одновременно.

Теоретическая часть

Алгоритм Ахо–Корасик — это эффективный алгоритм поиска подстрок, предназначенный для нахождения всех вхождений конечного множества шаблонных строк (словаря) во входном тексте. Разработанный Альфредом В. Ахо и Маргарет Дж. Корасик в 1975 году, он широко применяется в задачах, требующих одновременного поиска множества шаблонов, таких как текстовая обработка, биоинформатика и сетевая безопасность.

Описание задачи

Даны текстовая строка длины и множество из шаблонов , суммарная длина которых равна (сумма длин всех шаблонов). Алгоритм Ахо–Корасика находит все вхождения любого шаблона в . Конкретно, он возвращает все пары , такие что шаблон совпадает с подстрокой текста , начинающейся с позиции (т.е. ). Алгоритм особенно эффективен при одновременном поиске нескольких шаблонов, поскольку избегает многократного сканирования текста для каждого шаблона в отдельности.

Формальная постановка задачи:

Вход: текст и множество шаблонов .

Выход: все пары , такие что шаблон встречается в , начиная с позиции .

Применения

Алгоритм Ахо–Корасика используется в следующих областях:

  • системы полнотекстового поиска — для поиска множества ключевых слов в документах;

  • системы обнаружения вторжений — для сопоставления сигнатур вредоносных шаблонов в сетевом трафике;

  • биоинформатика — для поиска множества мотивов в последовательностях ДНК;

  • сжатие данных и обработка естественного языка — для анализа на основе шаблонов.

Основная часть

Поля данных

In [ ]:
# Все допустимые символы английского алфавита от 'a' до 'z'
const VALID_CHARS = collect('a':'z')
Out[0]:
26-element Vector{Char}:
 'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
 'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)
 'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)
 'd': ASCII/Unicode U+0064 (category Ll: Letter, lowercase)
 'e': ASCII/Unicode U+0065 (category Ll: Letter, lowercase)
 'f': ASCII/Unicode U+0066 (category Ll: Letter, lowercase)
 'g': ASCII/Unicode U+0067 (category Ll: Letter, lowercase)
 'h': ASCII/Unicode U+0068 (category Ll: Letter, lowercase)
 'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)
 'j': ASCII/Unicode U+006A (category Ll: Letter, lowercase)
 'k': ASCII/Unicode U+006B (category Ll: Letter, lowercase)
 'l': ASCII/Unicode U+006C (category Ll: Letter, lowercase)
 'm': ASCII/Unicode U+006D (category Ll: Letter, lowercase)
 'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)
 'o': ASCII/Unicode U+006F (category Ll: Letter, lowercase)
 'p': ASCII/Unicode U+0070 (category Ll: Letter, lowercase)
 'q': ASCII/Unicode U+0071 (category Ll: Letter, lowercase)
 'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
 's': ASCII/Unicode U+0073 (category Ll: Letter, lowercase)
 't': ASCII/Unicode U+0074 (category Ll: Letter, lowercase)
 'u': ASCII/Unicode U+0075 (category Ll: Letter, lowercase)
 'v': ASCII/Unicode U+0076 (category Ll: Letter, lowercase)
 'w': ASCII/Unicode U+0077 (category Ll: Letter, lowercase)
 'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)
 'y': ASCII/Unicode U+0079 (category Ll: Letter, lowercase)
 'z': ASCII/Unicode U+007A (category Ll: Letter, lowercase)

Структура узла конечного автомата

Определение структуры узла бора / конечного автомата Aho-Corasick

In [ ]:
mutable struct Node
    son::Vector{Int}   # номера дочерних вершин по каждому символу ('a'-'z')
    ans::Int           # количество совпадений, оканчивающихся здесь
    fail::Int          # ссылка на «предыдущий» узел с таким же суффиксом 
    du::Int            # число зависимых узлов, которые должны быть обработаны после текущего 
    idx::Int           # уникальный индекс для обнаруженного слова
end

Конструктор по умолчанию: создаём новый узел без дочерних узлов и с начальными значениями

In [ ]:
Node() = Node(zeros(Int, length(VALID_CHARS)), 0, 1, 0, 0)
Out[0]:
Node

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

Определение самого автомата Ахо-Корасика

In [ ]:
mutable struct ACAutomaton
    tr::Vector{Node}     # массив всех узлов дерева префиксного бора / автомата
    tot::Int             # общее количество использованных/существующих узлов
    final_ans::Vector{Int} # результат подсчета количества вхождений каждого шаблона
    pidx::Int            # счетчик уникальных индексов слов
end

Инициализация автомата с максимум max_nodes узлами и корнем

In [ ]:
ACAutomaton(max_nodes) = ACAutomaton([Node() for _ in 1:max_nodes], 1, Int[], 0)
Out[0]:
ACAutomaton

Операции над автоматом

Вставка паттерна в автомат

Функция добавления нового шаблона в автомат и присвоение ему уникального ID

In [ ]:
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
Out[0]:
insert (generic function with 1 method)

Построение "суффиксных" ссылок

Функция построения суффиксных переходов (fail links) – необходимы для корректной работы алгоритма AC

In [ ]:
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
Out[0]:
build (generic function with 1 method)

Поиск слов во входном тексте

Функция поиска всех слов в тексте с помощью построенного автомата

In [ ]:
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
Out[0]:
query (generic function with 1 method)

Расчёт итоговых результатов

После завершения основного прохода, рассчитываем точные значения совпадений для каждого шаблона, учитывая возможную зависимость между узлами через fail-ссылки

In [ ]:
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
Out[0]:
calculate_final_answers (generic function with 1 method)

Тестирование реализации

Функция, которая автоматически тестирует всю систему

In [ ]:
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
Out[0]:
test_aho_corasick (generic function with 1 method)

Запустим тестирование:

In [ ]:
# Несколько тестовых шаблонов
input = ["a", "bb", "aa", "abaa", "abaaa"]   
text = "abaaabaa"               # Текст для анализа

test_aho_corasick(input, text)
"Number of instances of 'a': " => 6
"Number of instances of 'bb': " => 0
"Number of instances of 'aa': " => 3
"Number of instances of 'abaa': " => 2
"Number of instances of 'abaaa': " => 1

Заключение

Мы рассмотрели реализацию алгоритма Ахо-Корасика на языке Julia, который позволяет эффективно находить все вхождения нескольких подстрок в текст за один проход. Эта реализация демонстрирует использование структур данных типа графа (дерево префиксов) вместе со специальными указателями (fail-ссылками) для быстрого перехода между состояниями.

Алгоритм особенно полезен для фильтрации текста, компиляции словарей, сетевых протоколов или любых других задач, где требуется параллельный анализ множества образцов. Реализованный код может быть расширен для более широкого спектра символов и использован в крупных NLP-проектах.

Пример разработан с использованием материалов Rosetta Code