每天学一个算法--Aho–Corasick 自动机

📘 教案 17:Aho--Corasick 自动机(AC 自动机 · 完整版)


一、问题动机(为何需要 AC)

给定一组模式串 ( \mathcal{P} = {p_1, p_2, \dots, p_k} ) 与一段文本 (T),目标是:

在 (T) 中同时找出所有模式串的出现位置(多模式匹配)

对比三种思路:

  • 逐个用 KMP:总复杂度约为 (O(|T|\cdot k))(不理想)
  • Trie:只能做前缀匹配,遇到失配无法高效"跳转"
  • AC 自动机 :在 Trie 上引入"失配转移",实现线性时间多模式匹配

二、总体结构(Trie + 失配指针)

AC 自动机由三部分组成:

  1. Trie(字典树):存储所有模式串
  2. fail 指针(失配指针):类似 KMP 的"回退信息"
  3. 自动机转移函数:定义在任意字符上的"下一状态"

核心思想:

当匹配失败时,不回退文本指针,而是沿 fail 指针跳转到"最长可复用前缀"继续匹配


三、形式化定义

设自动机状态集合为 Trie 上的节点集合 (V),根为 (root)。

  • (goto(u, c)):从状态 (u) 读入字符 © 的转移(Trie 边)
  • (fail(u)):失配指针,指向当前节点所表示字符串的最长"真后缀",且该后缀是某个模式串前缀对应的节点
  • (output(u)):在状态 (u) 结束的所有模式串(可能多个)

四、构建流程

4.1 构建 Trie

逐个插入模式串,记录每个单词结束的节点(可在节点上维护 output 列表)。


4.2 构建 fail 指针(BFS)

从根出发按层构建:

  • 根的子节点:fail = root
  • 其余节点 (u)(由父节点 § 经字符 © 到达):

fail(u) = goto\\big(fail§, c\\big) \\quad \\text{(若不存在则继续沿 fail 链回退,直到 root)}

同时:

output(u) ; \\mathrel{+}= ; output\\big(fail(u)\\big)

含义:如果在 (u) 失配后跳到 (fail(u)),那么在 (fail(u)) 上结束的模式同样应被报告


4.3 完整转移(可选优化)

为了实现"自动机"形式,可把所有缺失的 (goto) 转移补成:

goto(u, c) = \\begin{cases} \\text{原有子节点} \& \\text{若存在} goto\\big(fail(u), c\\big) \& \\text{否则(一直回退直到 root)} \\end{cases}

这样匹配阶段无需 while 回退,直接 O(1) 转移。


五、匹配过程

初始化 (state = root),逐字符扫描文本 (T):

对每个字符 (c = T[i]):

  1. 若无显式转移,沿 fail 指针回退直到存在转移或到达 root
  2. 执行转移:( state = goto(state, c) )
  3. 若 (output(state)) 非空,则报告所有匹配(这些模式串在位置 (i) 结束)

整个过程中文本指针从不回退


六、复杂度分析

  • 构建 Trie:(O(\sum |p_i|))
  • 构建 fail(BFS):(O(\Sigma \cdot |V|))(若补全转移;或按边数计 (O(\sum |p_i|)))
  • 匹配:(O(|T| + \text{匹配输出总数}))

总体近似线性。


七、关键直觉(对标 KMP)

  • KMP:在单个模式串上构建前缀函数(lps),失配时跳到"最长可复用前缀"
  • AC:在多模式串的 Trie上构建 fail,失配时跳到"最长可复用前缀对应的节点"

AC = KMP 思想在 Trie 上的推广


八、代码骨架(Python)

python 复制代码
from collections import deque, defaultdict

class Node:
    __slots__ = ("next", "fail", "output")
    def __init__(self):
        self.next = {}        # char -> Node
        self.fail = None      # fail pointer
        self.output = []      # list of pattern ids (or lengths)

class AhoCorasick:
    def __init__(self):
        self.root = Node()
        self.root.fail = self.root

    def insert(self, word, pid):
        node = self.root
        for ch in word:
            if ch not in node.next:
                node.next[ch] = Node()
            node = node.next[ch]
        node.output.append(pid)

    def build(self):
        q = deque()
        # 初始化 root 的直接子节点
        for ch, nxt in self.root.next.items():
            nxt.fail = self.root
            q.append(nxt)

        # BFS 构建 fail
        while q:
            u = q.popleft()
            for ch, v in u.next.items():
                f = u.fail
                # 沿 fail 链寻找可用转移
                while f is not self.root and ch not in f.next:
                    f = f.fail
                v.fail = f.next[ch] if ch in f.next else self.root
                # 继承输出
                v.output.extend(v.fail.output)
                q.append(v)

    def search(self, text):
        node = self.root
        results = []  # (end_index, pattern_id)
        for i, ch in enumerate(text):
            while node is not self.root and ch not in node.next:
                node = node.fail
            if ch in node.next:
                node = node.next[ch]
            else:
                node = self.root
            if node.output:
                for pid in node.output:
                    results.append((i, pid))
        return results

说明:若需要更快的常数,可将 next 改为定长数组(如 26/128),并在 build 中补全转移表。


九、典型应用

  • 敏感词过滤(同时匹配上万关键词)
  • 日志/流式文本多关键字扫描
  • 生物序列匹配(多模式)
  • 入侵检测(特征串匹配)

十、实现要点与坑

  • fail 的定义必须正确:基于父节点的 fail 逐级回退寻找
  • output 的合并不可遗漏:否则会漏报"后缀模式"
  • 字符集选择:小字符集用数组(快),大字符集用字典(省空间)
  • 内存占用:节点数约为所有模式串总长度之和
  • 去重/计数 :根据需求处理 output(可能同一位置命中多个模式)

十一、与 Trie / KMP 的关系总结

  • Trie:组织多字符串的前缀结构
  • KMP:单字符串的失配跳转
  • AC:在 Trie 上引入 KMP 式失配 → 多模式线性匹配

教学式结论

AC 自动机通过在字典树上引入失配指针,将"前缀共享"和"失配跳转"结合,使多模式匹配在一次线性扫描中完成,是字符串处理中的基础工程算法。

相关推荐
ID_180079054731 小时前
Python 实现京东商品详情 API 数据准确性校验(极简可直接用)
java·前端·python
re林檎2 小时前
八大排序算法(C++实现)
c++·算法·排序算法
淘气包海鸟2 小时前
雷达度量衡量
人工智能·算法·机器学习·信息与通信
睡觉就不困鸭2 小时前
第12天 多数元素
算法·哈希算法·散列表
xlq223222 小时前
46.线程池
linux·开发语言
LF男男2 小时前
Action- C# 内置的委托类型
java·开发语言·c#
练习时长一年2 小时前
@NotEmpty注解引发的报错
java·服务器·前端
cpp_25012 小时前
P2639 [USACO09OCT] Bessie‘s Weight Problem G
数据结构·算法·动态规划·题解·洛谷·背包dp
狂奔蜗牛飙车2 小时前
大数据赛项(中职组)-VMware+CentOS 7环境安装
linux·运维·centos·大数据应用与服务·大数据入门指南·中职组大数据应用及服务赛项·vmware中装centos7