Ac自动机
Ac自动机=Trie+Kmp
Kmp处理的是某个单词在文章中是否出现,出现次数,出现位置等问题,Ac自动机处理的是一堆单词在文章中是否出现,出现次数,出现位置等问题。
Ac自动机通过将一堆单词存储在Trie树中,并且对树上的每个节点建立失配指针ne[i]
(类似KMP对单词的每个字母建立失配指针),再将文章放在Trie树上跑,实现字符串的匹配问题。
失配指针如下图所示,红色表示失配指针指向。具体实现OI-wiki中的gif动图
模板题1:Acwing 1282 搜索关键词
给定n个单词,和一篇文章,问有多少个单词在文章中出现了。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=10010,S=55,M=1000010;
int n;
int tr[N*S][26],cnt[N*S],idx;
char str[M];
int q[N*S],ne[N*S];
inline void insert(){//插入过程和Trie的插入过程一模一样
int p=0;
for(int i=0;str[i];i++){
int t=str[i]-'a';
if(!tr[p][t])tr[p][t]=++idx;
p=tr[p][t];
}
cnt[p]++;
}
inline void build(){//构造树上失配指针ne[]
int hh=0,tt=-1;
for(int i=0;i<26;i++)
if(tr[0][i])
q[++tt]=tr[0][i];
//bfs,一层一层处理
while(hh<=tt){
int t=q[hh++];
for(int i=0;i<26;i++){
int p=tr[t][i];//将当前节点取出来
if(!p)tr[t][i]=tr[ne[t]][i];//如果当前节点为空,将不存在的字典树的状态等价到已经存在的另一状态,类比失配的状态
else {
ne[p]=tr[ne[t]][i];//如果trie中有这个节点,那么这个节点p的fail指针指向p的父节点t的fail指针指向的位置,父节点t的fail指针在p之前已经预处理过了,相当于动态规划。
q[++tt]=p;
}
}
}
}
int main(){
int t;cin>>t;
while (t--){
memset(tr,0,sizeof(tr));
memset(cnt,0,sizeof(cnt));
memset(ne,0,sizeof(ne));
idx=0;
cin>>n;
for(int i=0;i<n;i++){
cin>>str;
// cout<<str<<endl;
insert();
}
build();
cin>>str;//读入文章
// cout<<str<<endl;
int res=0;
//类似KMP的匹配过程,只不过这个过程变成了在树上实现
for(int i=0,j=0;str[i];i++){
int t=str[i]-'a';
j=tr[j][t];
int p=j;
while (p){
res+=cnt[p];
cnt[p]=0;//只需要判断是否出现过,不需要统计出现过多少次,所以当知道出现过的时候,就可以置0了
p=ne[p];
}
}
cout<<res<<endl;
}
return 0;
}
模板题2:ACwing 1285 单词
给出n个单词,问每个单词在这些单词中出现多少次。
思路:对给出的n个单词建立Trie树,单词每个字母走过的节点,该节点都要加一。最后统计的时候,从叶节点开始往上累加,后缀出现的次数累加到KMP对应前缀出现的次数上。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N =1e6+5;
int n;
int tr[N][26],f[N],idx;//f[]记录每个节点存了多少个相同的数
int q[N],ne[N];
char str[N];
int id[210];//记录每个单词的结束位置
inline void insert(int x){
int p=0;
for(int i=0;str[i];i++){
int t=str[i]-'a';
if(!tr[p][t])tr[p][t]=++idx;
p=tr[p][t];
f[p]++;//单词的每个字母在trie上出现的地方都要加1
}
id[x]=p;
}
void build(){
int hh=0,tt=-1;
for(int i=0;i<26;i++)
if(tr[0][i])q[++tt]=tr[0][i];
while(hh<=tt){
int t=q[hh++];
for(int i=0;i<26;++i){
int& p=tr[t][i];
if(!p)p=tr[ne[t]][i];
else {
ne[p]=tr[ne[t]][i];
q[++tt]=p;
}
}
}
}
int main(){
cin>>n;
for(int i=0;i<n;i++){
cin>>str;
insert(i);
}
build();
//队列中存储的顺序恰好是树的层次遍历顺序,相应后缀出现的次数累加到KMP对应前缀出现的次数
for(int i=idx-1;i>=0;i--)f[ ne[ q[i] ] ]+=f[ q[i] ];
for(int i=0;i<n;i++)cout<<f[id[i]]<<endl;
return 0;
}