【学习笔记】AC自动机(ACAM)
1.【代码】
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;int n;
int trans[N][26];
int cnt=0;
int fail[N];
int to[N];
int insert(string s){
int now=0;
for(auto v:s){
if(!trans[now][v-'a']) trans[now][v-'a']=++cnt;
now=trans[now][v-'a'];
}
return now;
}
vector<int> mp[N];
void Fail(){
queue<int> q;
for(int i=0;i<=25;i++){
if(trans[0][i]) q.push(trans[0][i]);
}
while(q.size()){
int u=q.front();q.pop();
//cout<<u<<" ";
for(int i=0;i<=25;i++){
if(trans[u][i]){
fail[trans[u][i]]=trans[fail[u]][i];
q.push(trans[u][i]);
}
else trans[u][i]=trans[fail[u]][i];//变成了trie图
}
}
for(int i=1;i<=cnt;i++) mp[fail[i]].push_back(i);
}
int num[N];
void dfs(int u){
for(auto v:mp[u]){
dfs(v);
num[u]+=num[v];
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
string s;cin>>s;
to[i]=insert(s);
}
Fail();
string s;cin>>s;
int now=0;
for(auto v:s){
now=trans[now][v-'a'];
num[now]++;
}
dfs(0);//暴力跳fail,转差分统计
for(int i=1;i<=n;i++) cout<<num[to[i]]<<"\n";
return 0;
}
2.【指针】
-
Trie指针,即字典树上的边
-
Fail指针:适配后的跳转指针
-
末指针:Fail指针的子集,只在末尾出现
3.【优化】
暴力跳fail,转为拓扑差分求和,这个优化是必须的
//朴素做法
// 每匹配一个字符,就暴力跳fail链
for(auto v:s){
now = trans[now][v-'a'];
int tmp = now;
while(tmp){
num[tmp]++;
tmp = fail[tmp];
}
}
//差分优化
// 匹配时:只记录节点访问次数
num[now]++;
// 匹配结束后:在fail树上累加
dfs(0); // 子节点贡献给父节点
4.【Trie树与Trie图】
构建 fail 时,我们将 \(trans[u][i]\) 缺失的边补全为 \(trans[fail[u]][i]\):
这样做的好处:匹配时无需判断边是否存在,直接 now = trans[now][c] 即可,时间复杂度严格 O(主串长度)。
坏处:原始 Trie 树结构被破坏,无法再进行 DFS 遍历。
5.【一些理解】
-
AC自动机的核心是fail指针建立,后面的query是在其上面做一些其他操作
-
trie和fail可以拆开,Trie 树上的祖先节点代表前缀;fail 指针指向的节点代表最长后缀,沿着 fail 链跳能得到所有后缀。
6.【应用】
6.1【自动机上dp】
P3041 [USACO12JAN] Video Game G
不可以 dfs(因为trie图有环),需要用分层DP
6.2【无限长不匹配】
标记所有模式串结束节点及其 fail 子树(这些节点不能走)
在剩余节点构成的 Trie 图上找环
如果存在环 → 可以无限走 → 答案存在
6.3【离线多组询问】
需要保留原始Trie树(用于DFS序)
在 fail 树上做树状数组/线段树维护
核心思想:fail 树上的子树代表所有以该节点为后缀的串

浙公网安备 33010602011771号