【AC自动机&&Trie图】积累
以前KMP和后缀系列(主要是后缀数组,后缀自动机),都刷了一定数量的题,但是对于AC自动机,却有些冷落,罪过。
但是我感觉,在蓝桥杯比赛中AC自动机出现的概率比后缀系列大,简单的会考匹配,稍难一点会考AC自动机+DP ,AC自动机+矩阵乘法,或者套其他算法blabla...
Trie图是AC自动机的改良版,不需要一直向上找fail。然后这里整理了一下Trie图的模板。
HihoCoder1036:Trie图 (时间在hihocoder上面排第一)。
题意:问长字符串里是否出现过字典里的单词。
#include<cstring> #include<cmath> #include<cstdio> #include<cstdlib> #include<iostream> #include<algorithm> using namespace std; const int maxn=1000010; char s[maxn]; int ch[maxn][26],num[maxn],Next[maxn]; int q[maxn],head,tail,cnt; struct ACauto { void insert() { int Now=0; for(int i=0;s[i];i++){ if(!ch[Now][s[i]-'a']) ch[Now][s[i]-'a']=++cnt; Now=ch[Now][s[i]-'a']; } num[Now]=1; } void build() { for(int i=0;i<26;i++){ if(ch[0][i]){ Next[ch[0][i]]=0; q[++head]=ch[0][i]; //if(num[Next[ch[0][i]]]) num[ch[0][i]]=1; } } while(tail<head){ int u=q[++tail]; for(int i=0;i<26;i++){ if(ch[u][i]){ q[++head]=ch[u][i]; Next[ch[u][i]]=ch[Next[u]][i]; //if(num[Next[ch[u][i]]]) num[ch[u][i]]=1; } else ch[u][i]=ch[Next[u]][i]; } } } bool find() { scanf("%s",s); int Now=0; for(int i=0;s[i];i++){ Now=ch[Now][s[i]-'a']; if(num[Now]) return true; } return false; } }Trie; int main() { int N,i,j; scanf("%d",&N); for(i=1;i<=N;i++){ scanf("%s",s); Trie.insert(); } Trie.build(); if(Trie.find()) printf("YES\n"); else printf("NO\n"); return 0; }
HDU2222 :Keywords Search (基础题型)
题意:问长字符串里出现了多少个字典里的单词。(字典里的单词可能重复,但是一个单词只统计一次)
思路:AC自动机统计以当前字母为后缀的单词数,并且沿fail指针一直统计,统计过后把num改为-1,避免重复统计。
#include<cstring> #include<cmath> #include<cstdio> #include<cstdlib> #include<iostream> #include<algorithm> using namespace std; const int maxn=1000010; char s[maxn]; int ch[maxn][26],num[maxn],Next[maxn]; int q[maxn],head,tail,cnt,ans; struct ACauto { void update() { head=tail=cnt=ans=0; memset(ch,0,sizeof(ch)); memset(num,0,sizeof(num)); memset(Next,0,sizeof(Next)); } void insert() { int Now=0; for(int i=0;s[i];i++){ if(!ch[Now][s[i]-'a']) ch[Now][s[i]-'a']=++cnt; Now=ch[Now][s[i]-'a']; } num[Now]++; } void build() { for(int i=0;i<26;i++){ if(ch[0][i]){ Next[ch[0][i]]=0; q[++head]=ch[0][i]; } } while(tail<head){ int u=q[++tail]; for(int i=0;i<26;i++){ if(ch[u][i]){ q[++head]=ch[u][i]; Next[ch[u][i]]=ch[Next[u]][i]; } else ch[u][i]=ch[Next[u]][i]; } } } void find() { scanf("%s",s); int Now=0; for(int i=0;s[i];i++){ Now=ch[Now][s[i]-'a']; int tmp=Now; while(tmp&&num[tmp]!=-1){ ans+=num[tmp]; num[tmp]=-1; tmp=Next[tmp]; } } return ; } }Trie; int main() { int T,N,i,j; scanf("%d",&T); while(T--){ Trie.update(); scanf("%d",&N); for(i=1;i<=N;i++){ scanf("%s",s); Trie.insert(); } Trie.build(); Trie.find(); printf("%d\n",ans); } return 0; }
HDU2896:病毒侵袭 (基础题型)
题意:给定N个病毒,M个网站,问对每个网站,带了哪些病毒,输出其编号。
思路:和上面差不多。注意memset会MTL。然后就是题目的ASCLL码范围的0到126。
#include<cstring> #include<cmath> #include<cstdio> #include<cstdlib> #include<iostream> #include<algorithm> using namespace std; const int maxn=100010; char s[maxn]; int ch[maxn][128],Next[maxn]; int q[maxn],num[maxn],head,tail,cnt,ans; struct ACauto { void update() { head=tail=cnt=ans=0; num[0]=Next[0]=0; for(int i=0;i<126;i++) ch[0][i]=0; } void insert(int opt) { int Now=0; for(int i=0;s[i];i++){ if(!ch[Now][s[i]]){ ch[Now][s[i]]=++cnt; for(int i=0;i<126;i++) ch[cnt][i]=0,num[cnt]=0,Next[cnt]=0; } Now=ch[Now][s[i]]; } num[Now]=opt; } void build() { for(int i=0;i<128;i++){ if(ch[0][i]){ Next[ch[0][i]]=0; q[++head]=ch[0][i]; } } while(tail<head){ int u=q[++tail]; for(int i=0;i<128;i++){ if(ch[u][i]){ q[++head]=ch[u][i]; Next[ch[u][i]]=ch[Next[u]][i]; } else ch[u][i]=ch[Next[u]][i]; } } } void find(int opt) { scanf("%s",s); int Now=0,x[10],size=0; for(int i=0;s[i];i++){ Now=ch[Now][s[i]]; int tmp=Now; while(tmp){ if(num[tmp]) x[++size]=num[tmp]; tmp=Next[tmp]; } } if(size){ ans++; sort(x+1,x+size+1); printf("web %d: %d",opt,x[1]); for(int i=2;i<=size;i++) printf(" %d",x[i]); printf("\n"); } return ; } }Trie; int main() { int N,Q,i,j; while(~scanf("%d",&N)){ Trie.update(); for(i=1;i<=N;i++){ scanf("%s",s); Trie.insert(i); } Trie.build(); scanf("%d",&Q); for(i=1;i<=Q;i++) Trie.find(i); printf("total: %d\n",ans); } return 0; }
但是上面那个需要一直沿fail指针上找,如果数据大一点就过不了。
改进是直接记录前缀‘和’,使不需要上找。
#include<cstring> #include<cmath> #include<cstdio> #include<cstdlib> #include<iostream> #include<algorithm> using namespace std; const int maxn=100010; char s[maxn]; int ch[maxn][128],Next[maxn]; int q[maxn],e[maxn][4],num[maxn],head,tail,cnt,ans; struct ACauto { void update() { head=tail=cnt=ans=0; num[0]=Next[0]=0; for(int i=0;i<126;i++) ch[0][i]=0; } void insert(int opt) { int Now=0; for(int i=0;s[i];i++){ if(!ch[Now][s[i]]){ ch[Now][s[i]]=++cnt; for(int i=0;i<126;i++) ch[cnt][i]=0,e[cnt][0]=0,Next[cnt]=0; } Now=ch[Now][s[i]]; } e[Now][++e[Now][0]]=opt; } void build() { for(int i=0;i<128;i++){ if(ch[0][i]){ Next[ch[0][i]]=0; q[++head]=ch[0][i]; } } while(tail<head){ int u=q[++tail]; for(int i=0;i<128;i++){ if(ch[u][i]){ q[++head]=ch[u][i]; Next[ch[u][i]]=ch[Next[u]][i]; for(int j=1;j<=e[ch[Next[u]][i]][0];j++){//记录前缀‘和 ’ e[ch[u][i]][++e[ch[u][i]][0]]=e[ch[Next[u]][i]][j]; } } else ch[u][i]=ch[Next[u]][i]; } } } void find(int opt) { scanf("%s",s); int Now=0,x[4],size=0; for(int i=0;s[i];i++){ Now=ch[Now][s[i]]; int tmp=Now; for(int j=1;j<=e[Now][0];j++) x[++size]=e[Now][j]; /*while(tmp){ if(num[tmp]) x[++size]=num[tmp]; tmp=Next[tmp]; }*/ } if(size){ ans++; sort(x+1,x+size+1); printf("web %d: %d",opt,x[1]); for(int i=2;i<=size;i++) printf(" %d",x[i]); printf("\n"); } return ; } }Trie; int main() { int N,Q,i,j; while(~scanf("%d",&N)){ Trie.update(); for(i=1;i<=N;i++){ scanf("%s",s); Trie.insert(i); } Trie.build(); scanf("%d",&Q); for(i=1;i<=Q;i++) Trie.find(i); printf("total: %d\n",ans); } return 0; }
HDU3065:病毒侵袭持续中 (基础题型)
题意:求每个单词在字符串里出现次数。
思路:和上一题差不多,记录路过单词的尾节点End的次数即可。字符串里不是大写字母的要回到根。
#include<cstring> #include<cmath> #include<cstdio> #include<cstdlib> #include<iostream> #include<algorithm> using namespace std; const int maxn=50010; char s[1010][60],x[2000010]; int ch[maxn][26],Next[maxn]; int q[maxn],End[maxn],num[1010],head,tail,cnt,ans; struct ACauto { void update() { memset(num,0,sizeof(num)); head=tail=cnt=ans=Next[0]=0; for(int i=0;i<26;i++) ch[0][i]=0; } void insert(int opt) { int Now=0; for(int i=0;s[opt][i];i++){ if(!ch[Now][s[opt][i]-'A']){ ch[Now][s[opt][i]-'A']=++cnt; Next[cnt]=End[cnt]=0; for(int i=0;i<26;i++) ch[cnt][i]=0; } Now=ch[Now][s[opt][i]-'A']; } End[Now]=opt; } void build() { for(int i=0;i<26;i++){ if(ch[0][i]){ Next[ch[0][i]]=0; q[++head]=ch[0][i]; } } while(tail<head){ int u=q[++tail]; for(int i=0;i<26;i++){ if(ch[u][i]){ q[++head]=ch[u][i]; Next[ch[u][i]]=ch[Next[u]][i]; } else ch[u][i]=ch[Next[u]][i]; } } } void find() { scanf("%s",x); int Now=0; for(int i=0;x[i];i++){ if(x[i]<'A'||x[i]>'Z') Now=0; else Now=ch[Now][x[i]-'A']; if(End[Now]) num[End[Now]]++; } } }Trie; int main() { int N,Q,i,j; while(~scanf("%d",&N)){ Trie.update(); for(i=1;i<=N;i++){ scanf("%s",s[i]); Trie.insert(i); } Trie.build(); Trie.find(); for(i=1;i<=N;i++) if(num[i]) { printf("%s: %d\n",s[i],num[i]); } } return 0; }
POJ2778: DNA Sequence (AC自动机+矩阵)
题意:给定几个病毒DNA,现在求有多少种长度位K的DNA,里面不含病毒DNA。
思路:路径(路径很长)种类一般都是需要矩阵优化的,这里把病毒DNA建立AC自动机,然后得到0可以到达的点(不形成病毒DNA)的和。
#include<cstdio> #include<cstdlib> #include<cstring> #include<iostream> #include<algorithm> using namespace std; #define ll long long const int Mod=100000; const int maxn=128; int ch[maxn][5],id[maxn],cnt; int q[maxn],head,tail,Next[maxn],tag[maxn]; char s[20]; ll ans; struct mat { ll mp[maxn][maxn]; mat(){memset(mp,0,sizeof(mp));} mat friend operator *(mat a,mat b) { mat res; for(int k=0;k<=cnt;k++) for(int i=0;i<=cnt;i++) for(int j=0;j<=cnt;j++){ res.mp[i][j]=(res.mp[i][j]+a.mp[i][k]*b.mp[k][j])%Mod; } return res; } mat friend operator ^(mat a,int x) { mat res; for(int i=0;i<=cnt;i++) res.mp[i][i]=1; while(x){ if(x&1) res=res*a; a=a*a; x>>=1; } return res; } }; mat array; struct ACautom { void insert() { int Now=0; for(int i=0;s[i];i++){ int x=id[s[i]]; if(!ch[Now][x]) ch[Now][x]=++cnt; Now=ch[Now][x]; } tag[Now]=1; } void build() { for(int i=0;i<4;i++){ if(ch[0][i]) q[++head]=ch[0][i]; if(!tag[ch[0][i]]) array.mp[0][ch[0][i]]++; } while(tail<head){ int u=q[++tail]; for(int i=0;i<4;i++){ if(ch[u][i]){ q[++head]=ch[u][i]; Next[ch[u][i]]=ch[Next[u]][i]; if(tag[Next[ch[u][i]]]) tag[ch[u][i]]=1; } else ch[u][i]=ch[Next[u]][i]; if(!tag[ch[u][i]]) array.mp[u][ch[u][i]]++; } } } void qpow(int K) { array=array^K; for(int i=0;i<=cnt;i++) ans=(ans+array.mp[0][i])%Mod; printf("%lld\n",ans); } }Trie; int main() { int N,K,i; id['A']=0; id['G']=1; id['C']=2; id['T']=3; scanf("%d%d",&N,&K); for(i=1;i<=N;i++) { scanf("%s",s); Trie.insert(); } Trie.build(); Trie.qpow(K); return 0; }
HDU2243:考研路茫茫——单词情结 (AC自动机+矩阵)
题意:问长度位1到L的所有单词中,有多少个不含给出的几个单词。
思路:和上一题差不多,但是要解决前缀和问题。有两种解决方案,一是二分;二是利用矩阵加一维,可以得到X^0+X^1+X^2...X^N。
比如得到26的0到N次幂和,就有矩阵a[0][0]=26,a[0][1]=1,a[1][0]=0,a[1][1]=1; 矩阵^N后,第一行的和就是答案。
#include<cstdio> #include<cstdlib> #include<cstring> #include<iostream> #include<algorithm> using namespace std; #define ull unsigned long long const int maxn=40; int ch[maxn][26],cnt; int q[maxn],head,tail,Next[maxn],tag[maxn]; char s[20]; struct mat { ull mp[maxn][maxn]; mat(){memset(mp,0,sizeof(mp));} mat init(){ memset(mp,0,sizeof(mp));} mat friend operator *(mat a,mat b) { mat res; for(int k=0;k<=max(cnt,2);k++) for(int i=0;i<=max(cnt,2);i++) for(int j=0;j<=max(cnt,2);j++) res.mp[i][j]+=a.mp[i][k]*b.mp[k][j]; return res; } mat friend operator ^(mat a,int x) { mat res; for(int i=0;i<=cnt;i++) res.mp[i][i]=1; while(x){ if(x&1) res=res*a; a=a*a; x>>=1; } return res; } }; mat array; struct ACautom { void update() { cnt=head=tail=0; memset(Next,0,sizeof(Next)); memset(tag,0,sizeof(tag)); memset(ch,0,sizeof(ch)); array.init(); } void insert() { int Now=0; for(int i=0;s[i];i++){ int x=s[i]-'a'; if(!ch[Now][x]) ch[Now][x]=++cnt; Now=ch[Now][x]; } tag[Now]=1; } void build() { for(int i=0;i<26;i++){ if(ch[0][i]) q[++head]=ch[0][i]; if(!tag[ch[0][i]]) array.mp[0][ch[0][i]]++; } while(tail<head){ int u=q[++tail]; for(int i=0;i<26;i++){ if(ch[u][i]){ q[++head]=ch[u][i]; Next[ch[u][i]]=ch[Next[u]][i]; if(tag[Next[ch[u][i]]]) tag[ch[u][i]]=1; } else ch[u][i]=ch[Next[u]][i]; if(!tag[ch[u][i]]) array.mp[u][ch[u][i]]++; } } cnt++; for(int i=0;i<=cnt;i++) array.mp[i][cnt]=1; } void qpow(int K) { ull ans,res=0; mat base; base.mp[0][0]=26; base.mp[0][1]=1; base.mp[1][0]=0; base.mp[1][1]=1; base=base^K; ans=base.mp[0][0]+base.mp[0][1]; array=array^K; for(int i=0;i<=cnt;i++) res=res+array.mp[0][i]; cout<<ans-res<<endl; } }Trie; int main() { int N,K; while(~scanf("%d%d",&N,&K)){ Trie.update(); for(int i=1;i<=N;i++) { scanf("%s",s); Trie.insert(); } Trie.build(); Trie.qpow(K); } return 0; }
HDU2825:Wireless Password (AC自动机+状压DP)
题意:对于M个单词,求长度为N的字符串,至少包含了K个单词的数量。
思路:AC自动机+状压DP。注意有可能有相同的字符串,所以End处经常会用到"|="。 dp[L][X][K]表示长度为L时,在AC自动机上X节点,已经包含字符串情况为K的种类数。 复杂度25*100*1<<10。
#include<queue> #include<cstdio> #include<cstdlib> #include<cstring> #include<iostream> #include<algorithm> using namespace std; const int maxn=110; const int Mod=20090717; int dp[26][maxn][1<<10]; int Next[maxn],ch[maxn][26],End[maxn],N,M,K,cnt; int que[maxn],head,tail; char s[maxn]; struct ACauto { void update() { cnt=head=tail=0; memset(ch,0,sizeof(ch)); memset(Next,0,sizeof(Next)); memset(End,0,sizeof(End)); memset(dp,0,sizeof(dp)); } void insert(int tag) { int Now=0; tag=1<<tag; for(int i=0;s[i];i++){ if(!ch[Now][s[i]-'a']) ch[Now][s[i]-'a']=++cnt; Now=ch[Now][s[i]-'a']; } End[Now]|=tag;//不是等于,一位可能两个相同的单词 } void build() { for(int i=0;i<26;i++){ if(ch[0][i]) que[++head]=ch[0][i]; } while(tail<head){ int u=que[++tail]; for(int i=0;i<26;i++){ if(ch[u][i]){ que[++head]=ch[u][i]; Next[ch[u][i]]=ch[Next[u]][i]; End[ch[u][i]]|=End[Next[ch[u][i]]]; } else ch[u][i]=ch[Next[u]][i]; } } } bool count(int x) { int res=0; for(int i=0;i<M;i++){ if((x>>i)&1) res++; } if(res>=K) return true; return false; } void solve() { dp[0][0][0]=1; for(int i=0;i<N;i++) for(int j=0;j<=cnt;j++) for(int p=0;p<(1<<M);p++){ if(dp[i][j][p]){ for(int x=0;x<26;x++){ int newj=ch[j][x]; int newp=p|End[ch[j][x]]; dp[i+1][newj][newp]+=dp[i][j][p]; dp[i+1][newj][newp]%=Mod; } } } int ans=0; for(int i=0;i<=cnt;i++) for(int j=0;j<(1<<M);j++){ if(count(j)) ans=(ans+dp[N][i][j])%Mod; } cout<<ans<<endl; } }Trie; int main() { while(~scanf("%d%d%d",&N,&M,&K)){ if(N==0&&M==0&&K==0) return 0; Trie.update(); for(int i=0;i<M;i++){ scanf("%s",s); Trie.insert(i); } Trie.build(); Trie.solve(); } return 0; }
HihoCder1640 : 命名的烦恼(AC自动机+状压DP)
题意:给定N个字符串,问最短的字符串长度,使之包含这N个字符串。
思路:用dis[u][k]表示走到u点,已经包含的字符串情况k。然后在单调优先队列在AC自动机上bfs找最短路。(由于u太大,当然直接bfs是超时了的,需要DP做)
//代码没有AC #include<queue> #include<cstdio> #include<cstdlib> #include<cstring> #include<iostream> #include<algorithm> using namespace std; const int maxn=50000; const int inf=0x7fffffff; int dp[1600][1<<15]; struct in { int dis,pos,opt; in(int x,int y,int z):dis(x),pos(y),opt(z){} friend bool operator < (in a,in b){ return a.dis>b.dis; } }; priority_queue<in>q; int Next[maxn],ch[maxn][26],End[maxn],N,cnt; int que[maxn],head,tail; char s[maxn]; struct ACauto { void insert(int tag) { int Now=0; for(int i=0;s[i];i++){ if(!ch[Now][s[i]-'a']) ch[Now][s[i]-'a']=++cnt; Now=ch[Now][s[i]-'a']; } End[Now]=1<<tag; } void build() { for(int i=0;i<26;i++){ if(ch[0][i]) que[++head]=ch[0][i]; } while(tail<head){ int u=que[++tail]; for(int i=0;i<26;i++){ if(ch[u][i]){ que[++head]=ch[u][i]; Next[ch[u][i]]=ch[Next[u]][i]; End[ch[u][i]]|=End[Next[ch[u][i]]]; } else ch[u][i]=ch[Next[u]][i]; } } } void solve() { dp[0][0]=0; q.push(in(0,0,0)); while(!q.empty()){ in tmp=q.top(); q.pop(); int u=tmp.pos, k=tmp.opt; for(int i=0;i<26;i++){ int v=ch[u][i],kk=k|End[v]; if(v==0) continue; if(kk==(1<<N)-1) { printf("%d\n",dp[u][k]+1); return ; } if(dp[v][kk]>dp[u][k]+1) { dp[v][kk]=dp[u][k]+1; q.push(in(dp[v][kk],v,kk)); } } } } }Trie; int main() { scanf("%d",&N); for(int i=0;i<N;i++){ scanf("%s",s); Trie.insert(i); } Trie.build(); for(int i=0;i<=cnt;i++) for(int j=0;j<(1<<N);j++) dp[i][j]=inf; Trie.solve(); return 0; }
HDU2457:DNA repair(AC自动机+DP)
题意:给定一些病毒DNA,和一串人的NDA,问至少改变多少个核苷酸,使之不含病毒DNA。
思路:AC自动机把病毒NDA表示出来,在DP的时候不经过病毒DNA。 dp[len][X]表示前len个字符,最后一个字符在AC自动机上X点的最优解,方程易得。
#include<queue> #include<cstdio> #include<cstdlib> #include<cstring> #include<iostream> #include<algorithm> using namespace std; const int maxn=1010; const int Mod=20090717; const int inf=1000000; int dp[maxn][maxn]; int Next[maxn],ch[maxn][4],End[maxn],N,cnt; int que[maxn],head,tail; char s[maxn]; int id[128],T; struct ACauto { void update() { cnt=head=tail=0; memset(ch,0,sizeof(ch)); memset(Next,0,sizeof(Next)); memset(End,0,sizeof(End)); } void insert() { int Now=0; for(int i=0;s[i];i++){ int x=id[s[i]]; if(!ch[Now][x]) ch[Now][x]=++cnt; Now=ch[Now][x]; } End[Now]=1; } void build() { for(int i=0;i<4;i++){ if(ch[0][i]) que[++head]=ch[0][i]; } while(tail<head){ int u=que[++tail]; for(int i=0;i<4;i++){ if(ch[u][i]){ que[++head]=ch[u][i]; Next[ch[u][i]]=ch[Next[u]][i]; End[ch[u][i]]|=End[Next[ch[u][i]]]; } else ch[u][i]=ch[Next[u]][i]; } } } void solve() { scanf("%s",s); int Len=strlen(s); int ans=Len; for(int i=1;i<=Len;i++) for(int j=0;j<=cnt;j++) dp[i][j]=inf; dp[0][0]=0; for(int i=0;s[i];i++){ int x=id[s[i]]; // cout<<i<<" "<<s[i]<<" "<<x<<endl; for(int j=0;j<=cnt;j++){ if(dp[i][j]==inf) continue; for(int p=0;p<4;p++){ if(End[ch[j][p]]) continue; if(p==x) dp[i+1][ch[j][p]]=min(dp[i+1][ch[j][p]],dp[i][j]); else dp[i+1][ch[j][p]]=min(dp[i+1][ch[j][p]],dp[i][j]+1); } } } for(int i=0;i<=cnt;i++) ans=min(ans,dp[Len][i]); printf("Case %d: ",++T); if(ans==Len) printf("-1\n"); else printf("%d\n",ans); return ; } }Trie; int main() { id['A']=0; id['G']=1; id['C']=2; id['T']=3; while(~scanf("%d",&N)&&N!=0){ Trie.update(); for(int i=0;i<N;i++){ scanf("%s",s); Trie.insert(); } Trie.build(); Trie.solve(); } return 0; }
It is your time to fight!