AC自动机解析
前置知识
建议彻底理解 Trie,这很重要。
介绍
AC 自动机(ACAM)是一种实现多模匹配的自动机(Automaton),将 KMP 思想和 Trie 融合起来,可以在 \(O(|s|+\sum|t_i|)\) 的时间,\(O(|s|+\sum|t_i|)\) 的空间内完成多模匹配。
原理
Fail 指针
这里的 Fail 指针和 KMP 的失配树中的 \(next\) 数组还是有区别的。KMP 的 \(next\) 指针是 Border,也就是最长相同真前后缀。而这里的 Fail 指针是指向最长的在 Trie 里的真后缀。
匹配
示意图:

其中插入的模式串为:\(t_1=\texttt{AB},t_2=\texttt{ABB},t_3=\texttt{ABC},t_4=\texttt{BC},t_5=\texttt{CA},t_6=\texttt{CBB},t_7=\texttt{CC}\),其中圈出的为 Trie 树上的标记。文本串是 \(s=\texttt{ABBCABACBCC}\)。
如果直接匹配就会发现当匹配到第三个字符时,刚好匹配上,此时匹配次数加一。然后呢?只能从根继续匹配。
我们想到优化:我们可以直接从最长的在 Trie 里的真后缀开始继续匹配,因为这样可以省去了前面的匹配,直接从 Fail 指针处继续。
如果没有在 Trie 里的真后缀怎么办?这就要从根开始了,也就是从头匹配。
匹配失败了也一样,跳 fail 直到回到根。
算法流程
以洛谷 P3808 AC 自动机(简单版)为例讲解。
建树
就是 Trie 本身的建树。不会可以去搜索一下。
代码:
cpp
void insert(const string &t) {
int now = 1;
for(auto &c : t) {
if(!trie[now].son[c - 'a']) trie[now].son[c - 'a'] = ++tc;
now = trie[now].son[c - 'a'];
}
trie[now].flg++;
}
求 fail 指针
方便表述,记 \(Fail_u\) 是 \(u\) 的 fail 指向的节点,\(S_u\) 是 \(u\) 指向的串。
我们可以发现两个性质:
- 真后缀的的真后缀是真后缀。
- 如果一个节点 \(u\) 和 \(Fail_u\) 都有一个相同儿子 \(i,i\in\{\texttt{A},\dots,\texttt{Z}\}\),那么 \(S_{Fail_u}+i\)( \(+\) 表示字符串拼接)就是\(s_u+i\) 的最长的在 Trie 里的真后缀。
所以我们就可以从深度小的串求出深度大的串的 fail 指针。我们发现这就类似 Trie 上 BFS。具体流程如下:
- 在访问到一个节点 \(u\) 的时候,遍历所有存在的子节点。
- 如果 \(u\) 的 fail 也有这个子节点,就直接将这个儿子的 fail 指向 \(u\) 的 fail 的这个子节点。否则重复跳直到根。
我们考虑继续优化。这里有一个技巧:路径压缩。
我们可以发现 \(Fail_u\) 总是在 \(u\) 之前计算,那我们就可以直接不管对方有没有这个子节点,就直接挂上去,也就是挂到 \(Fail_u\) 跳 fail 之后的节点。这就可以避免构建时每次跳 fail。
同样的,在 \(u\) 本来就没有的子节点,就可以直接挂到 \(Fail_u\) 的子节点上。这样在失配的时候就可以直接跳 fail 了。
为了搞定根节点的情况,我们规定 \(Fail_{root} = 0\),\(0\) 的所有子节点都是根。这样我们在更新第一层的时候,fail 就是 \(0\) 的儿子也就是根。
代码:
cpp
void build() {
queue<int> q;
for(int i = 0; i < 26; i++) trie[0].son[i] = 1;
q.push(1), trie[1].fail = 0;
while(!q.empty()) {
int fa = q.front(); q.pop();
for(int i = 0; i < 26; i++) {
int &son = trie[fa].son[i];
int fafail = trie[fa].fail;
if(!son) {son = trie[fafail].son[i]; continue;} // 没有的子节点直接跳 fail
trie[son].fail = trie[fafail].son[i]; // 直接挂上去即可
q.push(son);
}
}
}
查询
其实就是前面的匹配,但加入了路径压缩,在失配时就可以直接跳了。
cpp
int query(const string &s) {
int ans = 0, u = 1;
for(auto &_ : s) {
int c = _ - 'a', v = trie[u].son[c];
while(v > 1 && trie[v].flg != -1) { // 不断跳 fail,算过就不算了
ans += trie[v].flg; trie[v].flg = -1;
v = trie[v].fail;
}
u = trie[u].son[c];
}
return ans;
}
代码
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e6 + 5;
struct ACAM {
struct Node {
int son[26], fail, flg;
} trie[N];
int tc = 1;
void insert(const string &t) {
int now = 1;
for(auto &c : t) {
if(!trie[now].son[c - 'a']) trie[now].son[c - 'a'] = ++tc;
now = trie[now].son[c - 'a'];
}
trie[now].flg++;
}
void build() {
queue<int> q;
for(int i = 0; i < 26; i++) trie[0].son[i] = 1;
q.push(1), trie[1].fail = 0;
while(!q.empty()) {
int fa = q.front(); q.pop();
for(int i = 0; i < 26; i++) {
int &son = trie[fa].son[i];
int fafail = trie[fa].fail;
if(!son) {son = trie[fafail].son[i]; continue;}
trie[son].fail = trie[fafail].son[i];
q.push(son);
}
}
}
int query(const string &s) {
int ans = 0, u = 1;
for(auto &_ : s) {
int c = _ - 'a', v = trie[u].son[c];
while(v > 1 && trie[v].flg != -1) {
ans += trie[v].flg; trie[v].flg = -1;
v = trie[v].fail;
}
u = trie[u].son[c];
}
return ans;
}
}acam;
int main() {
ios::sync_with_stdio(0), cin.tie(0);
int n; cin >> n;
for(int i = 1; i <= n; i++) {
string t; cin >> t;
acam.insert(t);
}
acam.build();
string s; cin >> s;
cout << acam.query(s) << "\n";
return 0;
}