AC自动机
AC自动机,全称Aho-Corasick自动机。如果没记错的话好像就是前缀自动机。
其实AC自动机就是KMP上树的产物。理解了KMP,那AC自动机应该也是很好理解的。
与KMP类似,AC自动机也是扔一个字符走一步。当前状态始终只有一个,每次如何走都是确定的,换句话说AC自动机是一种确定型有限状态自动机(DFA)。
进行模式匹配是AC自动机的基本应用。如果稍加拓展一下,就可以知道在AC自动机上走k步就相当于产生了一个长为k、只包含给定字符集的字符串。借助这个性质,可以在AC自动机上DP来解决一些字符串有关问题。
注意一般应用的时候都会把节点不存在的儿子指向跳fail之后的结果,这样就可以在匹配时省掉fail,代码简洁的同时还能提速。
来点例题:
1. COGS1913 AC自动机
一道AC自动机模板题,没啥好说的。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int maxn=10010; 6 void insert(const char*,int); 7 void getfail(); 8 void match(); 9 void inc(int); 10 int n,ch[maxn][26]={{0}},val[maxn]={0},fail[maxn]={0},last[maxn]={0},q[maxn],head=0,tail=0,cnt=0,a[110]; 11 char s[110][60],c; 12 int main(){ 13 #define MINE 14 #ifdef MINE 15 freopen("ACautomata.in","r",stdin); 16 freopen("ACautomata.out","w",stdout); 17 #endif 18 scanf("%d",&n); 19 for(int i=1;i<=n;i++){ 20 scanf("%s",s[i]); 21 insert(s[i],i); 22 } 23 getfail(); 24 match(); 25 for(int i=1;i<=n;i++)printf("%s %d\n",s[i],a[i]); 26 #ifndef MINE 27 printf("\n-------------------------DONE-------------------------\n"); 28 for(;;); 29 #endif 30 return 0; 31 } 32 void insert(const char *c,int i){ 33 int x=0; 34 while(*c){ 35 if(!ch[x][*c-'a'])ch[x][*c-'a']=++cnt; 36 x=ch[x][*c-'a']; 37 c++; 38 } 39 val[x]=i; 40 } 41 void getfail(){ 42 int x,y; 43 for(int c=0;c<26;c++)if(ch[0][c])q[tail++]=ch[0][c]; 44 while(head!=tail){ 45 x=q[head++]; 46 for(int c=0;c<26;c++){ 47 if(ch[x][c]){ 48 q[tail++]=ch[x][c]; 49 y=fail[x]; 50 while(y&&!ch[y][c])y=fail[y]; 51 fail[ch[x][c]]=ch[y][c]; 52 last[ch[x][c]]=val[fail[ch[x][c]]]?fail[ch[x][c]]:last[fail[ch[x][c]]]; 53 } 54 else ch[x][c]=ch[fail[x]][c]; 55 } 56 } 57 } 58 void match(){ 59 char c; 60 int x=0; 61 for(;;){ 62 do c=getchar();while((c<'a'||c>'z')&&c!=EOF); 63 if(c==EOF)return; 64 x=ch[x][c-'a']; 65 if(val[x])inc(x); 66 else if(last[x])inc(last[x]); 67 } 68 } 69 void inc(int x){ 70 if(!x)return; 71 a[val[x]]++; 72 inc(last[x]); 73 }
2. POI2000 病毒
对模式串建AC自动机,问题就转化为了在AC自动机上是否存在一个不经过任何危险节点(单词节点或者存在last的节点)且无限长的路径。换句话说,就是问AC自动机上有没有环。
然后直接上dfs找环即可。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=30010; void insert(const char*,int=0); void init(); void dfs(int); int n,ch[maxn][2]={{0}},val[maxn]={0},fail[maxn]={0},last[maxn]={0},q[maxn],cnt=0,head=0,tail=0; bool vis[maxn]={false},ins[maxn]={false},ok=false; char c[maxn]; int main(){ #define MINE #ifdef MINE freopen("wir.in","r",stdin); freopen("wir.out","w",stdout); #endif scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%s",c); insert(c); } init(); dfs(0); if(!n||ok)printf("TAK"); else printf("NIE"); #ifndef MINE printf("\n-------------------------DONE-------------------------\n"); for(;;); #endif return 0; } void insert(const char* c,int x){ while(*c){ if(!ch[x][*c-'0'])ch[x][*c-'0']=++cnt; x=ch[x][*c-'0']; c++; } val[x]++; } void init(){ int x,y; for(int c=0;c<2;c++)if(ch[0][c])q[tail++]=ch[0][c]; while(head!=tail){ x=q[head++]; for(int c=0;c<2;c++){ if(ch[x][c]){ q[tail++]=ch[x][c]; y=fail[x]; while(y&&!ch[y][c])y=fail[y]; fail[ch[x][c]]=ch[y][c]; last[ch[x][c]]=val[ch[y][c]]?ch[y][c]:last[ch[y][c]]; } else ch[x][c]=ch[fail[x]][c]; } } } void dfs(int x){ if(val[x]||last[x])return; if(ins[x]){ ok=true; return; } if(vis[x])return; vis[x]=ins[x]=true; for(int c=0;c<2;c++){ if(ch[x][c]){ dfs(ch[x][c]); if(ok)return; } } ins[x]=false; }
3. COGS2248 情书
问你是否n个模式串都在文本串中出现过,AC自动机模板题。
话说string瞎搞居然比AC自动机快……
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int maxn=100010; 6 void insert(const char*,int); 7 void getfail(); 8 void match(const char*); 9 void inc(int); 10 int n,ch[maxn][26]={{0}},fail[maxn]={0},last[maxn]={0},val[maxn]={0},q[maxn],head=0,tail=0,cnt=0,a[110]; 11 char s[1350010],c; 12 bool ok; 13 int main(){ 14 #define MINE 15 #ifdef MINE 16 freopen("lettera.in","r",stdin); 17 freopen("lettera.out","w",stdout); 18 #endif 19 scanf("%d",&n); 20 for(int i=1;i<=n;i++){ 21 scanf("%*[^a-z]"); 22 scanf("%[a-z]",s); 23 insert(s,i); 24 } 25 getfail(); 26 for(;;){ 27 scanf("%*[^a-z]"); 28 if(scanf("%[a-z]",s)!=1)break; 29 fill_n(a+1,n,0); 30 match(s); 31 ok=true; 32 for(int i=1;i<=n;i++)if(!a[i]){ 33 ok=false; 34 break; 35 } 36 printf(ok?"Yes\n":"No\n"); 37 } 38 #ifndef MINE 39 printf("\n-------------------------DONE-------------------------\n"); 40 for(;;); 41 #endif 42 return 0; 43 } 44 void insert(const char *c,int i){ 45 int x=0; 46 while(*c){ 47 if(!ch[x][*c-'a'])ch[x][*c-'a']=++cnt; 48 x=ch[x][*c-'a']; 49 c++; 50 } 51 val[x]=i; 52 } 53 void getfail(){ 54 int x,y; 55 for(int c=0;c<26;c++)if(ch[0][c])q[tail++]=ch[0][c]; 56 while(head!=tail){ 57 x=q[head++]; 58 for(int c=0;c<26;c++){ 59 if(ch[x][c]){ 60 q[tail++]=ch[x][c]; 61 y=fail[x]; 62 while(y&&!ch[y][c])y=fail[y]; 63 fail[ch[x][c]]=ch[y][c]; 64 last[ch[x][c]]=val[fail[ch[x][c]]]?fail[ch[x][c]]:last[fail[ch[x][c]]]; 65 } 66 else ch[x][c]=ch[fail[x]][c]; 67 } 68 } 69 } 70 void match(const char *c){ 71 int x=0; 72 while(*c&&*c!='$'){ 73 x=ch[x][*c-'a']; 74 if(val[x])inc(x); 75 else if(last[x])inc(last[x]); 76 c++; 77 } 78 } 79 void inc(int x){ 80 if(!x)return; 81 a[val[x]]++; 82 inc(last[x]); 83 }
其实例题还有很多,只不过限于时间(zi ji tai cai)没做,在此贴上大概思路:
1. [SCOI 2012] 喵星球上的点名
大概思路肯定是建AC自动机然后模式匹配。但是有一个问题,模式串太多,暴力匹配肯定会T。
考虑每个节点的last,显然它们构成了一棵树。因此可以对last树做树上差分,最后一遍dfs求出每个模式串出现的次数。至于每次有多少模式串出现,直接用节点的深度就行了。
2. 文本生成器(08年俞华程论文题)
感觉并不用减法原理就可以写……(不过不一定正确,如果有发现不对的话请告诉我)
先建AC自动机,然后就是一个AC自动机上的DP。
令f[i][j][0/1]为从i出发再走k步的方案数,第三维0表示没有出现过模式串,1表示出现过了。
转移方程:
f[i][j][0]=sum{f[ch[i][c]][j-1][0]}
f[i][j][1]=sum{f[ch[i][c]][j-1][1]+f[ch[i][c]][j-1][0]}
边界:
f[0][0][0]=1,其余j=0情况均为0。
然后,显然这是一个线性递推式,而状态又不多,因此可以用矩阵快速幂优化。构造一个转移矩阵,然后乘乘乘就好了。
尽头和开端,总有一个在等你。