AC 自动机
AC 自动机
思路
我们知道,KMP 干的事是将一个字符串同另一个字符串匹配,但是如果有多个模式串和多个匹配串,那么 KMP 的时间复杂度就会来到 \(\mathcal O(n^2)\)。
那么怎么降低时间复杂度呢?
考虑到有多个字符串,我们先把匹配串建出来一颗字典树。
我们仿照 KMP 实现:
定义失配指针 \(fail_i\) ,指向 \(j\) 表示 \(j\) 是 \(i\) 的最长后缀(这里 \(i\),\(j\)均为字典树从根到节点构成的字符串)。
\(\boxed{构建 fail 指针}\):
我们考虑当前节点 \(u\),下一个字符 \(c\):
- 若 \(nxt[u][c] 存在\):\(fail_{nxt[u][c]} \to nxt[fail_u][c]\),这就相当于同时在 \(u\) 和 \(fail_u\) 后加一个 \(c\)。
- 若 \(nxt[u][c]\) 不存在:我们就一直跳 \(fail_u\),直到根节点。
具体解释看代码:
void calc(){
queue<int> q;
for(int i = 1; i <= 26; i ++) if(nxt[0][i]) q.push(nxt[0][i]); // 入队第一层
while(!q.empty()){
int u = q.front(); q.pop();
for(int i = 1; i <= 26; i ++){
if(nxt[u][i]) fl[nxt[u][i]] = nxt[fl[u]][i], in[nxt[fl[u]][i]] ++, q.push(nxt[u][i]); // 找到就更新
else nxt[u][i] = nxt[fl[u]][i]; // 找不到就跳(这里实际上是路径压缩)
}
}
}
当我们建完 AC 自动机以后,我们最重要的部分就做完了。
对于剩下的部分,有不同的做法。
应用
A. 拓扑排序求出现次数
对 AC 自动机跑一边模式串后进行拓扑排序,就可以获得每个匹配串在模式串中出现的次数。
void qry(){
int len = strlen(s + 1), p = 0;
for(int i = 1; i <= len; i ++) p = nxt[p][s[i] - 'a' + 1], ans[p] ++;
}
void topu(){
queue<int> q;
for(int i = 1; i <= idx; i ++) if(!in[i]) q.push(i);
while(!q.empty()){
int u = q.front(), v = fl[u]; q.pop();
ans[v] += ans[u], in[v] --; // 拓扑求答案
if(!in[v]) q.push(v);
}
}
B. AC 自动机上 DP
我们发现,连完 \(fail\) 的 AC 自动机本质上就是一个 DAG,故我们可以在 AC 自动机上 DP。
这类题比较套路,建出来 AC 自动机后 DP。
\(f_{i, j}\) 表示串长为 \(i\) ,当前在节点 \(j\) 时的状态。
for (int i = 0; i < m; i++) {
for (int j = 0; j <= idx; j++) {
for (int k = 1; k <= 26; k++) {
if (!vis[nxt[j][k]]) {
// 这里写转移
}
}
}
}
时间复杂度分析
-
建树
对于建树的时间复杂度,字典树:\(\mathcal O(n |S|)\),求 \(fail\) 指针:\(\mathcal O(n|S|)\)。其中 \(S\) 为全字符集,\(|S|\) 表示字符集的大小。
-
匹配
我们跑一个字符串 \(T\),时间复杂度 \(\mathcal O(|T|)\),\(|T|\) 表示 \(T\) 的串长。
-
DP
这个因题而定。一般至少有 \(\mathcal O(n|S|)\)。
例题
[P4052 JSOI2007] 文本生成器 - 洛谷
考虑减去不合法的情况,即串长为 \(m\) 且 \(j\) 不是所有串的结尾的方案数。
建 AC 自动机后简单 DP 即可。
for(int i = 0; i < m; i ++){
for(int j = 0; j <= idx; j ++){
for(int k = 1; k <= 26; k ++){
if(!vis[nxt[j][k]]){
(f[i + 1][nxt[j][k]] += f[i][j]) %= Mod;
}
}
}
}
[P3041 USACO12JAN] Video Game G - 洛谷
还是 \(f_{i, j}\)。
转移时,加上当前可以经过的字符串数量即可。
for(int i = 1; i <= m; i ++){
for(int j = 0; j <= idx; j ++){
for(int k = 1; k <= 3; k ++){
f[i][nxt[j][k]] = max(f[i][nxt[j][k]], f[i - 1][j] + get(nxt[j][k]));
}
}
}
[P2322 HNOI2006] 最短母串问题 - 洛谷
考虑在 AC 自动机上 BFS,找到的最短的经过所有字符串末节点的路径就是答案。
void bfs() {
for (int i = 0; i < 26; i++)
if (nxt[0][i])
q.push(nxt[0][i]);
while (!q.empty()) {
int x = q.front();
q.pop();
for (int i = 0; i < 26; i++) {
if (nxt[x][i]) {
fl[nxt[x][i]] = nxt[fl[x]][i];
q.push(nxt[x][i]);
val[nxt[x][i]] |= val[fl[nxt[x][i]]];
} else
nxt[x][i] = nxt[fl[x]][i];
}
}
}

浙公网安备 33010602011771号