AC自动机
void ins(char *s, int id) { int p=0; for(re i=0, len=strlen(s);i<len;++i) { int x=s[i]-'a'; if(!go[p][x])go[p][x]=++tot; p=go[p][x]; } //操作一下 } void build() { for(re i=0;i<26;++i) if(go[0][i]) Q.push(go[0][i]); while(!Q.empty()) { int x=Q.front(); Q.pop(); for(re i=0;i<26;++i) { if(go[x][i]) { int t=go[x][i]; fail[t] = go[fail[x]][i]; //操作一下 Q.push(t); } else go[x][i] = go[fail[x]][i]; } } } void query(char *s) { int p=0; for(re i=0, len=strlen(s);i<len;++i) { int x=s[i]-'a'; p=go[p][x]; cnt[p]++; } }
例题:
例1:AC自动机(二次加强版)
简要题意:给你一个文本串S和n个模式串,请你分别求出每个模式串在S中出现的次数。
分析:
这道题我们可不能直接暴力的用AC自动机,去匹配每一个模式串,这会TLE。
我们用一种很优秀的方法,拓扑排序!来解决这道题。
我们想象一下,模式串a与aa出现的次数,肯定a要比aa多。
同时,a可以为我们AC自动机中,aa的fail。
所以,我们用拓扑排序,从字典树下端推到上端,这样就可以了。
#include<bits/stdc++.h> using namespace std; #define re register int const int N=2e6+5; int go[N][26], fail[N], cnt[N], tot, bel[N], in[N]; queue<int>Q; void ins(char *s, int id) { // 加入模式串 int p=0; for(re i=0, len=strlen(s);i<len;++i) { int x=s[i]-'a'; if(!go[p][x])go[p][x]=++tot; p=go[p][x]; } bel[id] = p; // 标记第id个模式串的末尾在哪里,末尾的cnt个数,即为模式串出现的个数 } void build() { // 构建fail数组 for(re i=0;i<26;++i) if(go[0][i]) Q.push(go[0][i]); while(!Q.empty()) { int x=Q.front(); Q.pop(); for(re i=0;i<26;++i) { if(go[x][i]) { int t=go[x][i]; fail[t] = go[fail[x]][i]; in[fail[t]]++; // 处理拓扑排序 Q.push(t); } else go[x][i] = go[fail[x]][i]; } } } void solve() { // topsort for(re i=0;i<=tot;++i) if(in[i] == 0) Q.push(i); while(!Q.empty()) { int x=Q.front(); Q.pop(); int v=fail[x]; in[v]--; cnt[v]+=cnt[x]; if(in[v]==0)Q.push(v); } } void query(char *s) { int p=0; for(re i=0, len=strlen(s);i<len;++i) { int x=s[i]-'a'; p=go[p][x]; cnt[p]++; } } char s[5000000]; int main() { int n; scanf("%d",&n); for(re i=1;i<=n;++i) { scanf("%s",s); ins(s, i); } build(); scanf("%s",s); query(s); solve(); for(re i=1;i<=n;++i)printf("%d\n", cnt[bel[i]]); }
例2:电脑游戏
简要题意:你有n中可以得到贡献1的串,已知主串长度为k,求最大能得到的贡献。
例如:三个串"ABA","CB",和"ABACB",主串长度为5,若主串为"ABACB",则得到三分。
分析:
这道题很明显是在AC自动机上的dp。
我们定状态 dp[i][p],表示现在主串讨论到第i位,在AC自动机上的位置为p。
ps:这个“在AC自动机上的位置p”,其实是:最小的能用来表示主串长什么样子的值。
for(re i=0;i<m;++i) for(re j=0;j<=tot;++j) for(re k=0;k<3;++k) { int ch=go[j][k]; f[i+1][ch]=max(f[i+1][ch],f[i][j]+val[ch]); }
#include<bits/stdc++.h> using namespace std; #define re register int int go[5005][3], val[5005], fail[5005], tot; inline void insert(string s) { int p=0; for(re i=0,len=s.length();i<len;++i) { int x=s[i]-'A'; if(!go[p][x])go[p][x]=++tot; p=go[p][x]; } val[p]=1; } inline void build() { queue<int>Q; for(re i=0;i<3;++i)if(go[0][i])Q.push(go[0][i]); while(!Q.empty()) { int x=Q.front();Q.pop(); for(re i=0;i<3;++i) if(go[x][i]) { int t=go[x][i]; Q.push(t); fail[t]=go[fail[x]][i]; val[t]+=val[fail[t]]; } else go[x][i]=go[fail[x]][i]; } } int f[1005][5005]; signed main() { int n, m; cin>>n>>m; while(n--) { string s;cin>>s; insert(s); } build(); memset(f,-0x3f,sizeof(f)); f[0][0]=0;int ans=0; for(re i=0;i<m;++i) { for(re j=0;j<=tot;++j) { for(re k=0;k<3;++k) { int ch=go[j][k]; f[i+1][ch]=max(f[i+1][ch],f[i][j]+val[ch]); } } } for(re i=0;i<=tot;++i)ans=max(ans,f[m][i]); printf("%d",ans); }
例3:DNA修复
简要题意:DNA是一个用“ACGT”构成的字符串,其中有一些子串是不可以出现的,我们需要修改一些字母来满足要求。问最少需要修改的次数。
分析:这也是dp。
dp[i][p]表示讨论到主串第i位,当前主串在AC自动机的p位,满足要求所需的最小代价。
#include<bits/stdc++.h> using namespace std; #define re register int int go[1005][4], val[1005], fail[1005], tot; inline int gt(char x) { if(x=='A')return 0; if(x=='T')return 1; if(x=='G')return 2; return 3; } inline void insert(string s) { int p=0; for(re i=0,len=s.length();i<len;++i) { int x=gt(s[i]); if(!go[p][x])go[p][x]=++tot; p=go[p][x]; } val[p]=1; } inline void build() { queue<int>Q; for(re i=0;i<4;++i)if(go[0][i])Q.push(go[0][i]); while(!Q.empty()) { int x=Q.front();Q.pop(); for(re i=0;i<4;++i) if(go[x][i]) { int t=go[x][i]; Q.push(t); fail[t]=go[fail[x]][i]; val[t]|=val[fail[t]]; } else go[x][i]=go[fail[x]][i]; } } int f[1005][1005]; signed main() { int n;cin>>n; while(n--) { string s;cin>>s; insert(s); } build(); string S;cin>>S; memset(f,0x3f,sizeof(f));f[0][0]=0; int ans=1e9,m=S.length(); for(re i=0;i<m;++i) { for(re j=0;j<=tot;++j) { for(re k=0;k<4;++k) { int p=go[j][k]; if(!val[p])f[i+1][p]=min(f[i+1][p],f[i][j]+(gt(S[i])!=k)); } } } for(re i=0;i<=tot;++i)ans=min(ans,f[m][i]); printf("%d",(ans==1000000000?-1:ans)); }
例4:【SDOI2014 R1D1】数数
简要题意:
我们称一个正整数N是幸运数,当且仅当它的十进制表示中不包含数字串集合S中任意一个元素作为子串。例如当S={22,333,0233}时,233是幸运数,2333、20233、3223都不是幸运数。给定N和S,计算不大于N的幸运数个数。
分析:
这道题是 数位dp+AC自动机。kzsn有点说不清楚,以后应该会补。
#include<bits/stdc++.h> using namespace std; #define re register int const int mo=1e9+7; int go[1505][20], jud[1505], fail[1505], f[1505][1505][15], T[1505], tot; inline void insert(char *s) { int p=0; for(re i=0,len=strlen(s);i<len;++i) { int x=s[i]-'0'; if(!go[p][x])go[p][x]=++tot; p=go[p][x]; } jud[p]=1; } inline void build() { queue<int>Q; for(re i=0;i<10;++i)if(go[0][i])Q.push(go[0][i]); while(!Q.empty()) { int x=Q.front();Q.pop(); jud[x] |= jud[fail[x]]; for(re i=0;i<10;++i) { int t=go[x][i]; if(t) { Q.push(t); fail[t] = go[fail[x]][i]; } else go[x][i]=go[fail[x]][i]; } } go[0][0]=0; } char ch[1505]; signed main() { scanf("%s",ch+1); int len=strlen(ch+1), m;scanf("%d",&m); for(re i=1;i<=len;++i)T[i]=(int)(ch[i]-'0'); while(m--){scanf("%s",ch);insert(ch);} build(); f[0][0][1]=1; for(re i=0;i<len;++i) for(re j=0;j<=tot;++j) for(re l=0;l<=1;++l) if(f[i][j][l]) for(re k=(l==1?T[i+1]:9);k>=0;--k) if(!jud[go[j][k]]) { int t=go[j][k]; f[i+1][t][(l&(T[i+1]==k))] = (f[i+1][t][(l&(T[i+1]==k))] + f[i][j][l])%mo; } int ans = mo-1; for(re i=0;i<=tot;++i)ans = (ans + (f[len][i][0] + f[len][i][1]) % mo) % mo; printf("%d",ans); }
总结:
AC自动机,是一个可以用来匹配主串与模式串的优秀算法,常常可以用来帮助dp。
其中的fail数组也很是神奇,与kmp中的fail数组有异曲同工之妙。
kzsn还需多加练习。