AC自动机:初步理解可以理解为就是KMP加上trie AC自动机讲解超详细 - Hastieyua - 博客园 (cnblogs.com)
这个是模板:187 AC自动机 - 董晓 - 博客园 (cnblogs.com) 求是否出现过,只要匹配到就清0,不管重复的
https://www.bilibili.com/video/BV1tF41157Dy/?vd_source=23dc8e19d485a6ac47f03f6520fb15c2 董老师讲解的视频
重点就是:
ne[v]存的是节点v的回跳边的终点(四边形),ch[u][i]存的是节点u的树边或者转移边(三角形)
回跳边所指的是当前节点父节点回跳边指向节点的儿子
转移边所指的是当前节点的回跳边所指结点的儿子
和KMP的对比:
P3796 【模板】AC 自动机(加强版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
有 N个由小写字母组成的模式串以及一个文本串 T。每个模式串可能会在文本串中出现多次。你需要找出哪些模式串在文本串 T 中出现的次数最多。保证没有两个模式串重复
这里就需要处理重复出现次数
//AC自动机加强版 #include<bits/stdc++.h> #define maxn 1000001 using namespace std; char s[151][maxn],T[maxn]; int n,cnt,vis[maxn],ans; struct kkk{ int son[26],fail,flag; void clear(){memset(son,0,sizeof(son));fail=flag=0;} }trie[maxn]; queue<int>q; void insert(char* s,int num){ int u=1,len=strlen(s); for(int i=0;i<len;i++){ int v=s[i]-'a'; if(!trie[u].son[v])trie[u].son[v]=++cnt; u=trie[u].son[v]; } trie[u].flag=num; //变化1:标记为第num个出现的字符串 } void getFail(){ for(int i=0;i<26;i++)trie[0].son[i]=1; q.push(1);trie[1].fail=0; while(!q.empty()){ int u=q.front();q.pop(); int Fail=trie[u].fail; for(int i=0;i<26;i++){ int v=trie[u].son[i]; if(!v){trie[u].son[i]=trie[Fail].son[i];continue;} trie[v].fail=trie[Fail].son[i]; q.push(v); } } } void query(char* s){ int u=1,len=strlen(s); for(int i=0;i<len;i++){ int v=s[i]-'a'; int k=trie[u].son[v]; while(k>1){ if(trie[k].flag)vis[trie[k].flag]++; //如果有模式串标记,更新出现次数 k=trie[k].fail; } u=trie[u].son[v]; } } void clear(){ for(int i=0;i<=cnt;i++)trie[i].clear(); for(int i=1;i<=n;i++)vis[i]=0; cnt=1;ans=0; } int main(){ while(1){ scanf("%d",&n);if(!n)break; clear(); for(int i=1;i<=n;i++){ scanf("%s",s[i]); insert(s[i],i); } scanf("%s",T); getFail(); query(T); for(int i=1;i<=n;i++)ans=max(vis[i],ans); //最后统计答案 printf("%d\n",ans); for(int i=1;i<=n;i++) if(vis[i]==ans) printf("%s\n",s[i]); } }
P5357 【模板】AC 自动机(二次加强版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
求出每个模式串在串中的出现次数,注意这里两个模式串可能是重复的
可以用两种方法做,一种是从底向上,用拓扑排序,第二种是之间建fail树,然后求子树和,这两种方法都可以,重点是理解fail边变为树,以及怎样通过这种方法降低复杂度,避免重复跳fail边,导致超时
1、拓扑排序
#include <queue> #include <cstdlib> #include <cmath> #include <cstdio> #include <string> #include <cstring> #include <iostream> #include <algorithm> using namespace std; typedef long long ll; const int maxn = 2e6+100; int n,cnt,mp[maxn],in[maxn],ans,vis[maxn]; char s[maxn],t[maxn]; struct node{ int son[26],flag,ans,fail; void clea(){ memset(son,0,sizeof(son));fail=flag=ans=0; } }trie[maxn]; queue<int> q; void inser(char *s,int num){ int u=1,len=strlen(s); for(int i=0;i<len;i++){ int v=s[i]-'a'; if(!trie[u].son[v]) trie[u].son[v]=++cnt; u=trie[u].son[v]; } if(!trie[u].flag) trie[u].flag=num; mp[num]=trie[u].flag; //注意这里是在处理重复子串 //这道题有相同字符串要统计,设当前字符串是第i个,我们用一个Map[i]数组(不是STL那个)存((当前字符串在Trie中的那个位置)的flag), //最后把vis[Map[i]]输出就OK了。另外flag只在第一次赋值时变化,其他都不变。 } void get_fa(){ for(int i=0;i<26;i++) trie[0].son[i]=1; q.push(1); while(!q.empty()){ int u=q.front(); q.pop(); int Fail=trie[u].fail; for(int i=0;i<26;i++){ int v=trie[u].son[i]; if(!v){ trie[u].son[i]=trie[Fail].son[i];continue; } trie[v].fail=trie[Fail].son[i]; in[trie[v].fail]++; //从底到根算:拓扑排序 q.push(v); } } } void tuop(){ for(int i=1;i<=cnt;i++) if(in[i]==0) q.push(i); while(!q.empty()){ int u=q.front(); q.pop(); vis[trie[u].flag]=trie[u].ans; int v=trie[u].fail; in[v]--; trie[v].ans+=trie[u].ans; //向上传递值 //这里是拓扑排序了,如果入度为0了,也就是说他的儿子都算完了,就可以入队了 if(in[v]==0) q.push(v); } } void query(char *s){ int u=1,len=strlen(s); for(int i=0;i<len;i++){ u=trie[u].son[s[i]-'a']; trie[u].ans++; } } int main() { scanf("%d",&n); cnt=1; for(int i=1;i<=n;i++){ scanf("%s",s);inser(s,i); } get_fa(); scanf("%s",t); query(t); tuop(); for(int i=1;i<=n;i++) printf("%d\n",vis[mp[i]]); return 0; }
2、建fail树统计子树和
#include <cstdio> #include <iostream> #include <algorithm> #include<queue> #define base 139 const int maxn=2e5+10; using namespace std; const int maxm=2e6+10; typedef unsigned long long ll; char s[maxm]; queue<int> q; int head[maxn],nex[maxn],to[maxn],cnt; int n,tr[maxn][26],fail[maxn],match[maxn],size[maxn]; int tot=1; void dfs(int u){ for(int i=head[u];i;i=nex[i]){ int v=to[i]; dfs(v); size[u]+=size[v]; } } void add(int u,int v){//建边 nex[++cnt]=head[u]; to[cnt]=v; head[u]=cnt; } int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%s",s); int u=1; for(int j=0;s[j];j++){ int c=s[j]-'a'; if(!tr[u][c]) tr[u][c]=++tot; u=tr[u][c]; } match[i]=u; //记录每个模式串在 Trie 树上的终止节点 } for(int i=0;i<26;i++) tr[0][i]=1; q.push(1); while(!q.empty()){ int u=q.front();q.pop(); for(int i=0;i<26;i++){ if(tr[u][i]){ fail[tr[u][i]]=tr[fail[u]][i]; q.push(tr[u][i]); } else tr[u][i]=tr[fail[u]][i]; } } scanf("%s",s); for(int u=1,i=0;s[i];i++){ u=tr[u][s[i]-'a']; ++size[u]; //记录匹配次数 } for(int i=2;i<=tot;i++) add(fail[i],i); //建fail树 dfs(1); for(int i=1;i<=n;i++) printf("%d\n",size[match[i]]); return 0; }
1479:【例题1】Keywords Search
这个也是模板题
注意开的数据范围,重点是看getfail函数和que函数的写法(记住呀)
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1e4+10; const int M=1e6+10; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //AC自动机模板题 char s[M]; int ch[maxn*32][26]; int fail[maxn*32],flag[maxn*32]; int t,n,tot=0; void inti(){ memset(ch,0,sizeof(ch)); memset(flag,0,sizeof(flag)); memset(fail,0,sizeof(fail)); tot=0; } void inse(string s){ int now=0; for(int i=0;i<s.length();i++){ int x=s[i]-'a'; if(!ch[now][x]){ ch[now][x]=++tot; } now=ch[now][x]; } flag[now]++; //这里单词数+1 } void getfail(){ queue<int> q; for(int i=0;i<26;i++){ if(ch[0][i]) { fail[ch[0][i]]=0; q.push(ch[0][i]); } } while(!q.empty()){ int op=q.front(); q.pop(); for(int i=0;i<26;i++){ if(ch[op][i]){ fail[ch[op][i]]=ch[fail[op]][i]; q.push(ch[op][i]); } else ch[op][i]=ch[fail[op]][i]; } } } int que(string s){ int now=0,ans=0; for(int i=0;i<s.size();i++){ now=ch[now][s[i]-'a']; for(int j=now;j&&flag[j]!=-1;j=fail[j]){ ans+=flag[j]; flag[j]=-1; } } return ans; } int main(){ scanf("%d",&t); while(t--){ inti(); scanf("%d",&n); string tmp; for(int i=0;i<n;i++){ cin>>tmp; inse(tmp); } fail[0]=0; getfail(); scanf("%s",s); printf("%d\n",que(s)); } return 0; }
1480:玄武密码
重点还是理解数据结构啊。。。
我们只需要先建立所有密码的trie树
再以母串为主串跑一个AC自动机
不过其中还是有一些需要改动的地方
原本字典树中用来记录某个节点是不是字符串结尾的数组不需要,直接删去
我们需要另一个数组来标记哪些点被匹配
跑完ac自动机后从trie树上找最后一个匹配的点即可
优化:由于nxt数组是递归到0的所以只要有一个点被标记过,那么这个点到0的所有点都已经被遍历过直接退出即可
//为什么一个点超时啊。。。。
有的说是SAM好做一些,还不会先留着
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1e7+10; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; /* 我们只需要先建立所有密码的trie树 再以母串为主串跑一个AC自动机 不过其中还是有一些需要改动的地方 原本字典树中用来记录某个节点是不是字符串结尾的数组不需要,直接删去 我们需要另一个数组来标记哪些点被匹配 跑完ac自动机后从trie树上找最后一个匹配的点即可 优化:由于nxt数组是递归到0的所以只要有一个点被标记过,那么这个点到0的所有点都已经被遍历过直接退出即可 */ //为什么一个点超时啊。。。。 int ch[maxn][4]; int fail[maxn]; int flag[maxn]; int n,m,tot=0; char ss[maxn]; char sa[100050][150]; int jud(char x){ if(x=='E') return 0; else if(x=='S') return 1; else if(x=='W') return 2; else if(x=='N') return 3; } void inse(string s){ int now=0; for(int i=0;i<s.length();i++){ int x=jud(s[i]); if(!ch[now][x]){ ch[now][x]=++tot; } now=ch[now][x]; } //flag不用加了,因为是用来判断有没有访问过的 } void getfail(){ queue<int> q; for(int i=0;i<4;i++) { if(ch[0][i]) q.push(ch[0][i]); } while(!q.empty()){ int u=q.front();q.pop(); for(int i=0;i<4;i++){ if(ch[u][i]) fail[ch[u][i]]=ch[fail[u]][i],q.push(ch[u][i]); else ch[u][i]=ch[fail[u]][i]; } } } void biaoji(string s){ int u=0; for(int i=0;i<s.length();i++){ int c=jud(s[i]); u=ch[u][c]; for(int j=u;j;j=fail[j]){ if(flag[j]) break; flag[j]=1; } } } int getan(string s){ int u=0,ans=0; for(int i=0;i<s.length();i++){ int c=jud(s[i]); u=ch[u][c]; if(flag[u]) ans=i+1; //这里仔细看,求的是最长的匹配值,如果不匹配了就不会改变这个ans了,所以ans里面放的是最大的 } return ans; } int main(){ scanf("%d %d",&n,&m); scanf("%s",ss); for(int i=0;i<m;i++){ scanf("%s",sa[i]); inse(sa[i]); } getfail(); biaoji(ss); for(int i=0;i<m;i++){ printf("%d\n",getan(sa[i])); } return 0; }
1481:Censoring
就是给一个原串,给出n个模式串,匹配后删掉,注意事项是删掉一个后可能又能够匹配另一个串了
和KMP里面的一道题一样,这里能够通的过了...
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> #include<queue> using namespace std; char s[100001],ori[100001]; int n,tot,w,top; int trie[100001][26],fail[100001],heap[100001],sign[100001]; int isend[100001]; void insert(char *s){ int now=0,len=strlen(s); for(int i=0;i<len;i++){ int x=s[i]-'a'; if(!trie[now][x])trie[now][x]=++tot; now=trie[now][x]; } isend[now]=len; } void makefail(){ queue<int> q; for(int i=0;i<26;i++) if(trie[0][i])q.push(trie[0][i]); while(!q.empty()){ int now=q.front();q.pop(); for(int i=0;i<26;i++){ if(!trie[now][i]){ trie[now][i]=trie[fail[now]][i]; continue; } fail[trie[now][i]]=trie[fail[now]][i]; q.push(trie[now][i]); } } } void solve(char *s){ int now=0,len=strlen(s),i=0; w=0; while(i<len){ int x=s[i]-'a'; now=trie[now][x]; sign[++top]=now; heap[top]=i; if(isend[now]){ top-=isend[now]; if(!top) now=0; else now=sign[top]; } i++; } } int main() { scanf("%s",s); scanf("%d",&n); int len=strlen(s); for(int i=1;i<=n;i++){ scanf("%s",ori); insert(ori); } makefail(); solve(s); for(int i=1;i<=top;i++) printf("%c",s[heap[i]]); return 0; }
1482:单词
由模式串组成的原串
把所有字符串放在Trie里,并记cnt[i]为Trie的节点i为多少个字符串的前缀。
一个字符串是另一个字符串的子串,那么它也是该字符串某个前缀s[0,m]的后缀。
那么一个想法就出来了:
求出fail数组,然后以fail指针为边建出fail树,那么一个字符串的出现个数为:设它的结尾是节点x,那么fail树上以x为根的子树的cnt值的总和即为答案。
原文链接:https://blog.csdn.net/worldwide_d/article/details/51819862
//https://blog.csdn.net/worldwide_d/article/details/51819862
这道题也可以直接用AC自动机做,就是自己构建原串,在模式串中间加一个字符例如%
/* 把所有字符串放在Trie里,并记cnt[i]为Trie的节点i为多少个字符串的前缀。 一个字符串是另一个字符串的子串,那么它也是该字符串某个前缀s[0,m]的后缀。 那么一个想法就出来了: 求出fail数组,然后以fail指针为边建出fail树,那么一个字符串的出现个数为:设它的结尾是节点x,那么fail树上以x为根的子树的cnt值的总和即为答案。 原文链接:https://blog.csdn.net/worldwide_d/article/details/51819862 //https://blog.csdn.net/worldwide_d/article/details/51819862 */ #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<queue> const int maxn=1e6+10; using namespace std; int n,cnt,ch[maxn][26],size[maxn],fail[maxn]; int mp[maxn],h[maxn]; char s[maxn]; struct node{ void ins(int x){ scanf("%s",s+1); int now=0,len=strlen(s+1); for(int i=1;i<=len;i++){ int u=s[i]-'a'; if(!ch[now][u]) ch[now][u]=++cnt; now=ch[now][u]; size[now]++; // } mp[x]=now; //标记这个模式串在树上的位置 } void build(){ // queue<int> q; // for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]); // while(!q.empty()){ // int u=q.front();q.pop(); // for(int i=0;i<26;i++){ // if(tr[u][i]) { // fail[tr[u][i]]=tr[fail[u]][i]; // } // else ch[u][i]=tr[fail[u]][i]; // } // } //这里用手写队列 int head=0,tail=0; for(int i=0;i<26;i++) if(tr[0][i]) h[++tail]=ch[0][i]; while(head<tail){ int x=h[++head]; for(int i=0;i<26;i++){ int y=tr[x][i]; if(y) { fail[y]=tr[fail[x]][i]; h[++tail]=y; } else ch[x][i]=tr[fail[x]][i]; } } } void solve(){ for(int i=cnt;i>=0;i--) size[fail[f[i]]]+=size[h[i]]; for(int i=1;i<=n;i++) printf("%d\n",size[a[i]]); } }ac; int main(){ scanf("%d",&n); for(int i=1;i<=n;i++) ac.ins(i); ac.build(); ac.solve(); return 0; }
1483:最短母串
【题解】
类似于搜索+二进制记录状态的题目
搜索时利用BFS来跑,每一个结点的位置都可以用状态数组存起来,
判断是否为 (1<<n)- 1 即可。
在输出答案时需要递归实现,所以要一个辅助数组fa来记录上一个结点的位置。
洛谷 P2322 最短母串问题 状压+AC自动机_二货RK的博客-CSDN博客
#include<queue> #include<cstdio> #include<cstring> #include<algorithm> #include<iostream> using namespace std; const int maxn=6600; const int maxm=2e6+200; int tr[maxn][26],fail[maxn],end[maxn]; //这里END就是标记节点属于哪一条字符串的 int vis[maxn][605]; //这里是节点--状态 int fa[maxm],q[maxm],st[maxm]; //fa是为了标记上一个节点属于哪一个 //这个例子是可以模拟了,勉勉强强理解了 //?????编译错误 char str[maxm]; char mm[660]; int n,cnt; void prin(int x){ if(x==1) return; prin(fa[x]); printf("%c",str[x]+'A'); } void inse(char s[],int id){ int p=0; for(int i=0;s[i];i++){ int x=s[i]-'A'; if(!tr[p][x]){ tr[p][x]=++cnt; } p=tr[p][x]; } end[p]|=(1<<id); //结尾打上标记 使用的数位思想 } void build(){ int head=1,tail=0; for(int i=0;i<26;i++){ if(tr[0][i]){ q[++tail]=tr[0][i]; fail[tr[0][i]]=0; } } while(head<=tail){ int u=q[head++]; for(int i=0;i<26;i++){ if(tr[u][i]){ fail[tr[u][i]]=tr[fail[u]][i]; q[++tail]=tr[u][i]; end[tr[u][i]]|=end[fail[tr[u][i]]]; //这里也打上标记 } else tr[u][i]=tr[fail[u]][i]; } } } void solve(){ memset(q,0,sizeof(q)); int head=0,tail=1; q[1]=st[1]=0; //节点队列和状态队列 vis[0][0]=1; //预处理 while(head<tail){ int u=q[++head]; int s=st[head]; for(int i=0;i<26;i++){ int to=tr[u][i]; int ts=s|end[to]; if(vis[ts][to]) continue; //避免重复计算 fa[++tail]=head; q[tail]=to; str[tail]=i; //记录一下字符串 vis[ts][to]=1; st[tail]=ts; if(ts==(1<<n)-1) { prin(tail); printf("\n");return; } } } } int main(){ scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%s",mm); inse(mm,i); } build(); solve(); return 0; } #include<queue> #include<cstdio> #include<cstring> #include<algorithm> #include<iostream> using namespace std; const int N = 6e3+5; const int M = 2e6+50; const int Str_N = 605; int Trie[N][26],fail[N],End[N]; int vis[N][Str_N]; int Q[M],St[M]; int Fa[M]; char str[M]; int Head,Tail; int n,idx; char Str[Str_N]; void print(int x){ if(x==1) return ; print(Fa[x]); putchar(str[x]+'A'); } void Insert( char s[] , int Id ){ int p = 0 ; for(int i=0;s[i];i++){ int t = s[i]-'A'; if( !Trie[p][t] ) Trie[p][t] = ++idx; p = Trie[p][t]; } End[p] |= (1<<Id); //cout<<p<<" "<<End[p]<<endl; } void Build(){ Head = 1 , Tail = 0; for(int i=0;i<26;i++){ if( Trie[0][i] ){ Q[++Tail] = Trie[0][i]; fail[Trie[0][i]] = 0; } } //cout<<"now"<<endl; while( Head <= Tail ){ int u = Q[Head]; for(int i=0;i<26;i++){ int To = Trie[u][i]; if( To ){ fail[To] = Trie[fail[u]][i]; Q[++Tail] = To ; End[To] |= End[fail[To]]; }else{ Trie[u][i] = Trie[fail[u]][i]; } } //cout<<Q[Head]<<" "<<fail[Q[Head]]<<" "<<End[Q[Head]]<<endl; Head++; } } void Solve(){ memset(Q,0,sizeof Q ); Head = 0 , Tail = 1; Q[1] = St[1] = 0 ; vis[0][0] = 1 ; //cout<<"new"<<endl; while( Head < Tail ){ int u = Q[++Head],S = St[Head]; for(int i=0;i<26;i++){ int To = Trie[u][i]; int Ts = S | End[To] ; if( vis[Ts][To] ) continue; Fa[++Tail] = Head ; Q[Tail] = To ;str[Tail] = i; vis[Ts][To] = 1 ;St[Tail] = Ts ; /// cout<<To<<" "<<Ts<<" "<<Tail<<" "<<Fa[Tail]<<endl; if( Ts == (1<<n)-1 ){ print(Tail); putchar('\n'); return ; } } } } int main() { scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%s",Str); Insert(Str,i); } Build(); //cout<<endl<<endl; //for(int i=0;i<26;i++) cout<<Trie[4][i]<<" "; // cout<<endl<<endl; Solve(); return 0; } /* 96 * 97 4 98 HNOI 99 NOIP 100 NOI 101 IOI 102 103 HNOIPIOI 104 */