每天学一个算法--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 = Ti):

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

相关推荐
地平线开发者8 小时前
J6B vio scenario sample
算法
七歌杜金房9 小时前
我终于又有了自己的 Linux 电脑
linux·debian·mac
Flittly15 小时前
【AgentScope Java新手村系列】(16)从RAG到多路检索
java·spring boot·spring
小兔崽子去哪了15 小时前
Java 生成二维码解决方案
java·后端
BothSavage20 小时前
Trae远程开发中DeepSeek自定义模型4054错误的排查与修复
算法
小林ixn20 小时前
从暴力到KMP:一道题彻底搞懂字符串匹配的前世今生
算法
人活一口气20 小时前
从JVM调优到MCP协议:Java全栈技术体系深度总结与企业级架构实践
java·spring boot
烬羽21 小时前
字符串算法入门:从反转字符串到回文判断,面试不再慌
算法·面试
NE_STOP1 天前
Vibe Coding -- 完整项目案例实操
java