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

初始化具有最多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)

构建"后缀"链接

构建后缀转换(失败链接)的功能对于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)          # 用于存储每个模板的Id的数组
    
    # 将所有模板添加到计算机
    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项目中使用。

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