Engee 文档
Notebook

Aho-Korasik算法

本示例考察了Julia编程语言中Aho-Korasik算法的实现。 该算法旨在在同一时间搜索文本中的多个样本。

理论部分

[算法Ахо-Корасик](https://ru.wikipedia.org/wiki/Алгоритм_Ахо_—_Корасик )是一种高效的子字符串搜索算法,旨在查找输入文本中有限模板字符串集(字典)的所有出现。 由Alfred V.Aho和Margaret J.设计,成立于1975年,广泛用于需要同时搜索多个模式的任务,如文本处理,生物信息学和网络安全。

任务说明

给出一个文本字符串 长度 还有很多 模板 ,其总长度为 (所有图案的长度之和)。 Aho-Korasik算法查找任何模式的所有出现 . 具体来说,它返回所有对。 ,使得模板 匹配文本的子字符串 从位置开始 (即 该算法在同一时间搜索多个模板时特别有效,因为它避免了每个模板单独的多个文本扫描。

问题的正式陈述:

输入:文本 还有很多模板 .

输出:所有对 ,使得模板 它发生在 从位置开始 .

申请表格

Aho-Korasik算法用于以下领域:

*全文搜索系统-用于搜索文档中的多个关键字;

*入侵检测系统-用于匹配网络流量中恶意模式的签名;

*生物信息学-在DNA序列中搜索多个基序;

*数据压缩和自然语言处理-用于基于模板的分析。

主要部分

数据字段

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)

有限状态机节点的结构

Bohr节点/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

Aho-Korasik自动机的结构

Aho-Korasik自动机本身的定义

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

具有最大_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)

构建"后缀"链接

构造后缀转换(失败链接)的功能对于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)

最终结果的计算

完成主传递后,我们计算每个模板的精确匹配值,同时考虑到节点之间可能通过失败链接的依赖性。

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

结论

我们已经回顾了Aho-Korasik算法在Julia中的实现,这使得在一次传递中有效地查找文本中多个子字符串的所有出现成为可能。 此实现演示了图形类型数据结构(前缀树)以及用于快速状态转换的特殊指针(失败链接)的使用。

该算法对于文本过滤、字典编译、网络协议或任何其他需要并行分析多个样本的任务特别有用。 实现的代码可以扩展到更广泛的字符范围,并在大型NLP项目中使用。

该示例是使用[罗塞塔代码]的材料开发的(https://rosettacode.org/wiki/Aho–Corasick_algorithm