终于看懂啦!终于学会了AC自动机啦!(Flag立起)写篇博客梳理一下自己的思路吧。
在文章的开头,必须先放上一些大佬的博客来压压场面:当然是orz ZZK大佬,神犇传送门
然而AC自动机的原理其实是比较好理解的,就是把Trie树和KMP的思想结合起来。这么说起来KMP就是单个字符串的AC自动机啊。(雾)
给出许多模式串,把这些字符串都加入到Trie树中,在每个字符串结尾的叶子节点上计数。
然后在所有模式串都加入Trie树中后开始建立next树,也就是对于所有的模式串做KMP。
首先把和根节点直接相连的节点加入队列中,然后用BFS的思想来确定每个节点的next:取当前的队头,然后枚举这个节点的所有儿子,设当前儿子的字母为ch,当前节点的next为k,取所有存在的儿子,若节点k的ch儿子不为空,则当前节点的儿子的next就等于节点k的ch儿子的编号,否则k=next[k],即保证节点k到根的路径是当前节点到根节点路径的后缀,重复上述操作,直到确定了当前节点的ch儿子。
不过上述求next树的方法过于暴力,可能会TLE,那么我们就会想到上述操作是否有可以优化的地方,答案当然是有的。
依然是去当前的队头,枚举这个节点的所有儿子,若这个儿子存在,那么这个儿子的next就等于节点k的ch儿子,否则这个儿子就等于节点k的ch儿子。
为什么这样的操作可以保证正确性呢?因为我们可以考虑节点k的ch儿子,如果节点k的ch儿子不存在,则把它变成了一个指针,指向它如果存在时匹配的节点。
这样搞了以后就非常方便,不需要每次再向上枚举,浪费时间。
最后就是查询,这题的查询就是求给定的字符串中包含了多少模式串,只要每次向上枚举,答案加上当前节点的计数,然后把当前节点标记为不可取即可。
附上AC代码:
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N=1000010;
struct note{
int nt,ed,lk[26];
}AC[N];
int n,size=0;
char s[N];
inline void insert(char *s){
int len=strlen(s+1),now=0;
for (int i=1; i<=len; ++i){
if (!AC[now].lk[s[i]-'a']) AC[now].lk[s[i]-'a']=++size;
now=AC[now].lk[s[i]-'a'];
}
++AC[now].ed;
return;
}
inline void build(){
queue <int> que;
for (int i=0; i<26; ++i)
if (AC[0].lk[i]) AC[AC[0].lk[i]].nt=0,que.push(AC[0].lk[i]);
while (!que.empty()){
int p=que.front();que.pop();
for (int i=0; i<26; ++i)
if (AC[p].lk[i]) AC[AC[p].lk[i]].nt=AC[AC[p].nt].lk[i],que.push(AC[p].lk[i]);
else AC[p].lk[i]=AC[AC[p].nt].lk[i];
}
return;
}
inline int query(char *s){
int len=strlen(s+1);
int now=0,ans=0;
for (int i=1; i<=len; ++i){
now=AC[now].lk[s[i]-'a'];
for (int j=now; j&&AC[j].ed!=-1; j=AC[j].nt)
ans+=AC[j].ed,AC[j].ed=-1;
}
return ans;
}
int main(void){
scanf("%d",&n);
for (int i=1; i<=n; ++i) scanf("%s",s+1),insert(s);
AC[0].nt=0;build();
scanf("%s",s+1),printf("%d",query(s));
return 0;
}