学习笔记:AC 自动机
AC 自动机,全称 Aho-Corasick 自动机 (Aho-Corasick Automaton),简称 ACAM,一般用于解决多模式串匹配问题:给定字典 \(s_1,\cdots,s_n\) 和文本串 \(t\),求每个 \(s_i\) 在 \(t\) 中的出现次数。
AC 自动机的构建
基础
AC 自动机的构建结合了 Trie 和 KMP 算法:先将所有模式串插入到 Trie 中,然后利用 KMP 算法的思想构建失配指针(fail 指针)。
下面我们将 Trie 上的节点 \(u\) 称为状态,同时我们不区分 \(u\) 和 \(u\) 在 Trie 上表示的字符串。
状态 \(u\) 的 fail 指针 \(fail_u\) 指向状态 \(v\),其中 \(v\) 是 \(u\) 的在 Trie 中出现的真后缀中最长的一个。这样,当 \(u\) 的下一个字符失配(即当前试图寻找的下一位在 Trie 中没有出现)时,可以通过跳到 \(fail_u\) 来快速进行下一个模式串的匹配。
注意 AC 自动机的 fail 指针与 KMP 中的 next 指针(或者说 \(\pi\) 函数)的异同:
- 相同点:都是失配时用于快速跳转的指针;
- 不同点:\(nxt_i\)(或 \(\pi(i)\))指向最长相等真前后缀(最长 border),而 \(fail_i\) 指向所有模式串的前缀中与 \(i\) 的真后缀相等且最长的一个。
由于 fail 指针一定是往前跳,跳到的节点深度一定小于当前节点,因此我们在 Trie 中 BFS 求 fail 指针。
如果由父亲 \(p\) 通过字符 \(c\) 连接到节点 \(u\),记为 \(u=tr(p,c)\)。类似于 KMP 算法,考虑如何在 \(p\) 和 \(fail_p\) 的基础上求出 \(fail_u\)。
我们要求 \(fail_u\) 代表的字符串和 \(u\) 的后缀相等且最长,而我们已知 \(fail_p\) 代表的字符串和 \(p\) 的后缀相等且最长,因此尝试令 \(fail_u=tr(fail_p,c)\),如果 \(fail_p\) 的下一条边没有连出字符 \(c\),那就退而求其次,用 \(fail(fail_p)\) 去尝试,直到为根节点(代表空串)。
字典图
如果单纯跳 fail 指针,可能会出现跳的次数太多以至于效率降低的情况,通过字典图优化可以解决这一问题。
所谓字典图优化,就是将原来的字典树结构进行扩展,将原本不存在的转移加入。
具体来说,我们的目标是将每个节点 \(i\) 对所有字符 \(c\) 的转移 \(\delta(i,c)\) 处理出来,从而一步跳到下一个转移点。过程如下:
- BFS 建立 AC 自动机,对当前节点 \(i\) 枚举所有字符 \(c\)(假设前面若干层的字典图已经构建完毕):
- 如果 \(tr(i,c)\) 在 Trie 树中,则令 \(fail_{tr(i,c)}=tr(fail_i,c)\);
- 否则,令 \(\delta(i,c)=\delta(fail_i,c)\)。
构建 AC 自动机的函数如下:
void build() {
queue<int> q;
for (int i = 0; i < 26; i++)
if (tr[i][0]) q.push(tr[i][0]);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = 0; i < 26; i++) {
if (tr[i][u]) {
fail[tr[i][u]] = tr[i][fail[u]];
q.push(tr[i][u]);
} else {
tr[i][u] = tr[i][fail[u]];
}
}
}
return;
}
形式化地,设 \(Q\) 为 Trie 树上所有状态的集合,根节点 \(0\in Q\) 表示空串,构造字典图的 AC 自动机的 *转移函数**如下:
AC 自动机的接受状态集合 \(F\),即为向 Trie 中插入单词时最后的节点(称为终止节点)构成的集合。
AC 自动机的 fail 树(to do)
AC 自动机的 fail 指针对应的边 \((i,fail_i)\) 构成了一棵有根树,可以对其进行各种树上操作,从而解决一些问题。
根据定义,\(p\) 是 \(q\) 的祖先,当且仅当 \(p\) 代表的字符串 \(t_p\) 是 \(q\) 代表的字符串 \(t_q\) 的后缀。

浙公网安备 33010602011771号