字符串之 AC 自动机
3 AC 自动机 / ACAM
自动机是什么?
此部分根据 oi-wiki 整理。自动机的数学定义是一个五元组,它精确地描述了一个离散的、状态驱动的计算模型。
其形式化定义如下:
一个确定有限状态自动机(DFA) 由一个五元组 \((Q, \Sigma, \delta, q_0, F)\) 构成:
1. \(Q\):一个有限的状态集合。
2. \(\Sigma\):一个有限的输入字母表(字符集)。
3. \(\delta\):转移函数,定义为 \(\delta: Q \times \Sigma \rightarrow Q\)。它描述了自动机在读取一个字符后,如何从当前状态唯一地转移到下一个状态。
4. \(q_0\):起始状态,且 \(q_0 \in Q\)。
5. \(F\):接受状态集合,且 \(F \subseteq Q\)。
自动机的工作过程是:从起始状态 \(q_0\) 开始,根据输入字符串中的每个字符,反复应用转移函数 \(\delta\) 来更新当前状态。当所有字符都被读取后,如果最终状态属于接受状态集合 \(F\),则该字符串被“接受”,否则被“拒绝”。
AC 自动机(Aho–Corasick automaton)可以解决一类多模匹配问题。
AC 自动机求解多模匹配问题的思想为:
把模式串插入到 Trie 中,通过构建类似 KMP 的失配指针(\(fali\) 指针),然后扫描文本串在 AC 自动机上匹配得到结果。
3.1 如何构建 AC 自动机
3.1.1 构建字典树
首先,我们需要将这若干个模式串插入到一个 Trie 中。
对于 Trie 树上每个位置,在自动机中的状态为 Trie 的根节点到当前节点的字符串。
以下用 \((u, c)\) 表示 \(u\) Trie 上由 \(c\) 指向的节点。
3.1.2 如何求解 \(fail\) 指针
和 KMP 中的失配指针相比,AC 自动机的失配指针 \(fail_u\) 指向的为 Trie 树上的一个状态 \(v\),满足 \(v\) 是 \(u\) 的最长后缀,也就是 \(u\) 在 Trie 树上的后缀状态中最长的那个。
考虑当前我们需要求解的位置为 \(u\),父亲为 \(fa\),有一条边 \((fa, c)\) 指向 u$$,其中 \(c\) 是字符集中的字符,并且满足深度 \(< u\) 的节点已经求解完成。
首先,因为状态 \(u\) 相当于状态 \(fa\) 加上了一个字符 \(c\),所以 \(u\) 的后缀都相当于:状态 \(fa\) 的后缀加上字符 \(c\)。
由于我们需要求的是最长后缀,那么最先尝试的肯定是 \((fail_{fa}, c)\),接下来考虑 \((fail_{fa}, c)\) 是否存在。
- 若 \((fail_{fa}, c)\) 存则,则 \(fail_u = (fail_{fa}, c)\)。
- 否则,我们不断尝试当前 \(fa\) 的次长后缀,也就是 \(fail_{fail_{fa}}\),如果存在则 \(fail_u = (fail_{fail_{fa}}, c)\),否则像这样一直跳。
- 跳到根则设为 \(0\)。
3.1.3 代码实现
构建字典树部分就不说了。
求解 \(fail\) 指针部分,由于我们需要小于当前深度的点全部求解完毕,所以使用 BFS 实现。
对于 \((fail_{fa}, c)\) 存在的情况,则直接赋值即可。
对于 \((fail_{fa}, c)\) 不存在的情况,你可以直接网上跳,但是这里介绍一种叫做路径压缩的方法,也叫建立字典图,貌似更快。
这种情况,我们要找的实际上就是 \((fail^k(fa), c)\)。
\(fail^k\) 表示跳 \(k\) 次 \(fail\) 指针。
我们可以一步到位存储这个结果,具体而言,就是直接令:
对于状态 \((fail_{fa}, c)\),它如果缺失转移,会被压缩为第一个存在的 \((fail^k(fail_{fa}), c)\),那么,这样压缩后,\((fa, c)\) 相当于第一个存在的 \((fail^{k + 1}(fail_fa), c)\),于是我们成功完成了一层转移。

浙公网安备 33010602011771号