AC自动机模板(拓扑优化)
AC自动机模板(拓扑优化)
时间复杂度: \(O( \sum \left | T_{i} \right | + \left | S \right | )\)
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int tot=1,n,vis[N],Map[N],in[N];//编号从1开始;vis最终每个原始词编号的出现次数;Map[i]表示第i个原始词实际对应的那个flag
string T,S;
queue<int> q;
struct node{
int son[26],flag,fail,cnt;//26个小写字母;flag为这个是哪个词的终点;cnt为访问计数
}tr[N];
void insert(string s,int num){//建Trie树
int p=1;
for(int i=0;i<s.size();i++){
int u=s[i]-'a';
if(!tr[p].son[u]) tr[p].son[u]=++tot;
p=tr[p].son[u];
}
if(!tr[p].flag) tr[p].flag=num;//还没访问过就设为当前词编号
Map[num]=tr[p].flag;//因为有重复的词,所以重复的也指向这里
}
void query(string s){//优化后,直接打标记就行(直接计数)
int p=1;
for(int i=0;i<s.size();i++){
p=tr[p].son[s[i]-'a'];//这里注意要转换为整型
tr[p].cnt++;//直接打上标记
}
}
void getfail(){//设置失配指针(失配指针:当你匹配不了更长的前缀时,就退而求其次,匹配尽可能长的、已经出现过的一个真后缀作为新的起点)
for(int i=0;i<26;i++){
tr[0].son[i]=1;//初始化0的所有儿子都是1,这样任何从0号点的跳转,都会回到起点,很方便
}
q.push(1);//将根压入队列
while(q.size()){
int u=q.front();q.pop();
int Fail=tr[u].fail;//就是fafail
for(int i=0;i<26;i++){//遍历所有儿子
int v=tr[u].son[i];//处理u的i儿子的fail,这样就可以不用记父亲了
if(!v){//情况A:没有这个儿子
tr[u].son[i]=tr[Fail].son[i];//直接路径压缩,直接向前跳(因为BFS按深度一层层处理,所以一定跳转目标已经被处理好)
continue;
}//情况B:有这个儿子
tr[v].fail=tr[Fail].son[i];//设置失配指针(标准的AC自动机fail构建规则:当前字符串的最长后缀(v去掉第一个字符)等于已有前缀(Fail的i子节点代表的串))
in[tr[v].fail]++;//记得加上入度
q.push(v);//存在实节点才压入队列
}
}
}
void topu(){//向上汇总
for(int i=1;i<=tot;i++){
if(in[i]==0){
q.push(i);//将入度为0的点全部压入队列里(即叶子)
}
}
while(q.size()){//从叶子向根传递消息
int u=q.front();q.pop();
vis[tr[u].flag]=tr[u].cnt;//如果有flag终点标记,就把累计的cnt存进vis数组
int v=tr[u].fail;in[v]--;//将唯一连出去的出边fail的入度减去(拓扑排序的操作)
tr[v].cnt+=tr[u].cnt;//更新fail的cnt值,因为v是u的后缀,所以每访问到u,一定也访问到了v
if(in[v]==0){
q.push(v);//拓扑排序常规操作
}
}
}
int main(){
cin.tie(0)->ios::sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>T;
insert(T,i);
}
getfail();
cin>>S;query(S);
topu();
for(int i=1;i<=n;i++){
cout<<vis[Map[i]]<<'\n';//原词可能重复,Map[i]找到了它实际对应的flag,然后输出那个flag的答案
}
return 0;
}

浙公网安备 33010602011771号