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];
        }
    }
}
posted @ 2026-07-22 11:56  Hty111  阅读(4)  评论(0)    收藏  举报