例题
介绍
处理字符串问题时有一个非常强大的工具——AC自动机。在我看来,AC自动机就是字典树和字符串后缀有机结合,产生的一个强大的工具。
大致的思路解释
众所周知,当你拥有n个字符串和一个目标字符串时,你可以先构建字典树,然后在树上快速查询目标串是否是你拥有的字符串或某个字符串的前缀,抑或者是否存在一个字符串是目标串的前缀。
但当遇到询问这n个字符串中有多少个是目标串的子串时,光靠字典树时间会爆,若是依靠KMP算法,时间上也过不去,那怎么办呢?AC自动机就是用来解决这个问题的!
首先,我们需要建立一棵由n个字符串构成的字典树。
然后,我们需要求出字典树上每个节点代表的字符串的最长后缀并打好转移标记,用fail[]数组转移。
最后,我们只需要将目标串放在字典树上跑,然后把经过的每一个节点的后缀以及后缀的后缀都统计到答案中即可。
具体操作请见代码。
完整代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,t[N][30],fail[N],tot,cnt[N];
string c,s;//string的使用速度快于char
void add(string c){//建立基本的字典树,并标记单词结尾
int p=0;
for(int i=0;i<c.size();i++){
if(!t[p][c[i]-'a']) t[p][c[i]-'a']=++tot;//动态开点
p=t[p][c[i]-'a'];
}
cnt[p]++;//统计以p结点为结尾的单词数量
}
void build(){//在字典树的基础上加边,使其可以快速查询到最长后缀
queue<int> q;
for(int i=0;i<26;i++){
if(t[0][i]) q.push(t[0][i]);//寻找第一层已经建立了的点
}
while(q.size()){
int x=q.front();q.pop();
for(int j=0;j<26;j++){
if(t[x][j]){//该点所代表的字符串加上该字符后仍已建立
fail[t[x][j]]=t[fail[x]][j];//新字符串的最长后缀一定是该点所代表的字符串的最长后缀加上该字符
q.push(t[x][j]);//新字符串已经被更新,可以用于更新其它字符串
}
else{
t[x][j]=t[fail[x]][j];//新字符串不存在,则转移到该点所对应的字符串的最长后缀加上该字符串
}
}
}
}
int ask(string c){//查询
int sum=0,x=0;
for(int i=0;i<c.size();i++){
x=t[x][c[i]-'a'];//在字典树上跑目标串
for(int j=x;j&&cnt[j]!=-1;j=fail[j]){//跑字串并统计数量
sum+=cnt[j];cnt[j]=-1;//打标记,防止重复
}
}
return sum;
}
int main(){
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>c;
if(c.size()) add(c);
}
build();
cin>>s;
cout<<ask(s)<<endl;
return 0;
}