AC来自一个大佬的名字,并不是写了就可以自动AC的意思 XD
AC自动机是建立在trie树上的一种优化手段。trie在每次查询一个字符串时,如果在一个子树查不到就会回溯再查,效率会很低。我们考虑在给每个节点加一个如果查不到就跳转的指针fail,那么如果找不到的话直接跳转到fail就可以了。fail代表的是拥有该点的最大后缀的点的位置。
那么怎么寻找这个fail呢?因为我们要寻找最大后缀,深度就是一个比较重要的条件。于是我们bfs。注意了,设一个点now,tmp为now的子节点(从a到z任何一个),有以下推导:
(第一段使用结构体存储了点,第二段是用数组)
e[tmp].fail=e[e[now].fail].nxt[i];
fail[tmp]=ch[fail[now]][i];
首先我们知道trie是有一个0节点的。那么0连接的所有点的fail就连接的是0。如果我们接下来遍历每个点,对于任何点的fail都是其父节点的fail的子字符。因为是bfs,所以这样做一定是正确的而且一定会找到其最长后缀的点。
这里注意一下:如果now点有该字符是按照上面记录的。如果没有(ch[now][i]==0)就需要转移记录一下nxt:
e[now].nxt[i]=e[e[now].fail].nxt[i];
ch[now][i]=ch[fail[now]][i];
这样的话我们查询的时候只需要令now不断等于当前点的nxt就可以快速遍历啦~如果遍历到0,说明就end了。
让我们在打板子的时候顺便A一下洛谷的模板题:
P3808 【模板】AC自动机(简单版)
#include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #include<queue> using namespace std; const int maxn=1e6+10; char s[maxn]; int n,fail[maxn],ch[maxn][26],vis[maxn],val[maxn]; struct ac{ int cnt=0; inline void insert(char *s) { int now=0; for(;*s;s++) { int t=*s-'a'; if(!ch[now][t])ch[now][t]=++cnt; now=ch[now][t]; } val[now]++; } inline void build() { int now=0,t,tmp; queue<int> q; q.push(0); while(!q.empty()) { now=q.front();q.pop(); for(int i=0;i<26;i++) { t=ch[now][i]; if(t){ if(now) fail[t]=ch[fail[now]][i]; q.push(t); }else ch[now][i]=ch[fail[now]][i]; } } } inline int match(char *s) { int now,ans=0; for(now=0;*s;s++) { int t=*s-'a'; now=ch[now][t]; for(int tmp=now;tmp and !vis[tmp];tmp=fail[tmp]) ans+=val[tmp],vis[tmp]=1; } return ans; } }ac; int main() { scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%s",s); ac.insert(s); } ac.build(); scanf("%s",s); printf("%d ",ac.match(s)); return 0; }