一直对AC自动机有种执念,觉得有了它就能自动AC,是一种特别神奇的算法,今天终于见识了。
实际上,学习了AC自动机以后,才发现并没有那么玄乎。
一、Trie(字典树/前缀树)
学习AC自动机前,需要学会字典树。字典树,顾名思义当然是一棵树。特殊地,这棵树上的边是字符。那么对于任意一条从起点到某一结点的路径,都对应唯一的字符串。这样我们就可以用字典树上的结点来表示字符串。
如图,路径0-1经过“a",那么结点1就对应字符串“a";路径0-1-2-3依次经过“a","b","c",那么结点3就对应字符串"abc";路径0-1-4-5依次经过“a","a","a",那么结点5就对应字符串"aaa"……
从中我们可以发现,某一结点对应的字符串其实就是从结点0到该结点路径上的字符顺次连接而成的串。
在字典树上我们可以方便地实现字符串的插入、查找。
对于树上的结点,我们可以保存一些附加信息。如果我们规定结点的权值,用非零值表示单词结点,我们就可以用一棵字典树表示出一个由字符串组成的集合,并完成一些有用的工作,比如词频统计、前缀匹配等。
1 struct Trie { 2 static const int maxn=(int)(1e5)+5; 3 static const int sigma_size=26; 4 int ch[maxn][sigma_size]; //儿子结点 5 int val[maxn]; //结点权值 6 int siz; 7 Trie():siz(0) { 8 memset(ch,0,sizeof(ch)); 9 memset(val,0,sizeof(val)); 10 } 11 int ord(char c) { 12 return c-'a'; 13 } 14 //插入 15 void insert(char* s,int v) { 16 int u=0,n=strlen(s); 17 for(int i=0; i<n; i++) { 18 int c=ord(s[i]); 19 if(!ch[u][c]) ch[u][c]=++siz; 20 u=ch[u][c]; 21 } 22 val[u]=v; 23 } 24 //查找 25 int find(char* s) { 26 int u=0,n=strlen(s); 27 for(int i=0; i<n; i++) { 28 int c=ord(s[i]); 29 if(!ch[u][c]) return 0; 30 u=ch[u][c]; 31 } 32 return val[u]; 33 } 34 };
二、KMP算法(单模式匹配)
KMP算法是一个单模式匹配算法,由三个发现者D.E.Knuth,J.H.Morris和V.R.Pratt的名字命名。
如果真的要用一篇文章来叙述KMP算法的思想,估计10多页的论文都说不完。这真是一个十分优美而又简洁的算法,虽然有点难于理解,但若是深入了解它的工作过程,一定会惊叹于它的精妙绝伦。
朴素算法进行单模式匹配,最坏情况下复杂度是O(nm);而KMP算法是O(n+m)。是什么大大加速了匹配过程?核心之处在于失配指针。朴素的方法之所以复杂度较高,在于它做了许多没有必要的比较。试想一下,如果模板串“abbababb"已经匹配到“abbabab"了,只有最后一个字符不一样,那我们要全部重新来过吗?
不用,既然已经匹配了“abbabab",那么前面两个字符一定是“ab",只要继续匹配模板串的第三个字符就好了。
如图是失配时的状态转移图,其中虚线箭头就是所谓的失配指针。当匹配某一字符失败时,只要转移到失配指针指向处继续匹配即可。具体的算法过程不再赘述,附上代码一份:
1 struct KMP { 2 static const int maxn=(int)(1e5)+5; 3 int f[maxn]; //失配指针 4 bool ok[maxn]; //是否成功匹配 5 int n,m; 6 //计算失配指针 7 void getFail(char* P) { 8 f[0]=0; 9 for(int i=1,j=0; i<m; i++) { 10 while(j && P[i]!=P[j]) j=f[j-1]; 11 if(P[i]==P[j]) j++; 12 f[i]=j; 13 } 14 } 15 //单模式匹配 16 void Find(char* T,char* P) { 17 n=strlen(T); 18 m=strlen(P); 19 getFail(P); 20 for(int i=0,j=0; i<n; i++) { 21 while(j && T[i]!=P[j]) j=f[j-1]; 22 if(T[i]==P[j]) j++; 23 if(j==m) ok[i]=true; 24 } 25 } 26 };
三、AC自动机(多模式匹配)
观察下图,如果我们把虚线箭头全部去掉,其实就是第一部分中的Trie;而那些虚线箭头,貌似是第二部分中的失配指针?
Trie+失配指针,就基本上组成了传说中的AC自动机。AC自动机,由其发现者Aho Corasick的名字命名,用于解决多模式匹配的问题。
可以说,AC自动机就是KMP算法以Trie为基础的实现。其中失配指针的作用也和KMP算法中的一样,写法也类似。唯一有些不同的是,在单模式匹配中,由于只有一个模板串,成功匹配也就是一个串;但是多模式匹配时,有可能在成功匹配一个模板串时,也同时成功匹配了另一个模板串。这个问题可以用后缀链接进行处理。
1 struct AC_AutoMaton { 2 static const int maxn=(int)(1e5)+5; 3 static const int sigma_size=26; 4 int ch[maxn][sigma_size]; 5 int f[maxn],last[maxn]; 6 int cnt[maxn],val[maxn]; 7 int siz; 8 9 AC_AutoMaton():siz(0) { 10 memset(ch,0,sizeof(ch)); 11 memset(val,0,sizeof(val)); 12 } 13 14 int ord(char c) { 15 return c-'a'; 16 } 17 //插入 18 void insert(char* s,int v) { 19 int u=0,n=strlen(s); 20 for(int i=0; i<n; i++) { 21 int c=ord(s[i]); 22 if(!ch[u][c]) ch[u][c]=++siz; 23 u=ch[u][c]; 24 } 25 val[u]=v; 26 } 27 //计算失配指针 28 int getFail() { 29 queue<int> Q; 30 f[0]=0; 31 for(int c=0; c<sigma_size; c++) { 32 int u=ch[0][c]; 33 if(u) { 34 f[u]=last[u]=0; 35 Q.push(u); 36 } 37 } 38 while(!Q.empty()) { 39 int k=Q.front(); 40 Q.pop(); 41 for(int c=0; c<sigma_size; c++) { 42 int u=ch[k][c]; 43 if(!u) continue; 44 Q.push(u); 45 int v=f[k]; 46 while(v && !ch[v][c]) v=f[v]; 47 f[u]=ch[v][c]; 48 last[u]=val[f[u]] ? f[u] : last[f[u]]; 49 } 50 } 51 } 52 //统计 53 void add(int u) { 54 for(; u; u=last[u]) cnt[val[u]]++; 55 } 56 //多模式匹配 57 void Find(char* s) { 58 int u=0,n=strlen(s); 59 memset(cnt,0,sizeof(cnt)); 60 for(int i=0; i<n; i++) { 61 int c=ord(s[i]); 62 while(u && !ch[u][c]) u=f[u]; 63 u=ch[u][c]; 64 if(val[u]) add(u); 65 else if(last[u]) add(last[u]); 66 } 67 } 68 };
四、优化:Trie图
注意到,在AC自动机匹配时,每一步可能有多次回溯(直到找到一个可以继续匹配的结点)。那么我们是否可以在之前就完成一些工作,使每步只需要转移一次呢?
我们引入Trie图的概念。Trie图是一个确定性有限状态自动机(DFA),比起AC自动机,增加了确定性的属性。即在任一位置,输入任一字符集中的字符,都可以转移到一个确定的结点。
实现方法很简单,就是把getFail()中的 if(!u) continue; 改成 if(!u) {ch[k][c]=ch[f[k]][c];continue;} ,然后就可以把Find()中 while(u && !ch[u][c]) u=f[u]; 删去。实质上就是通过添加有向边,使所有的转移统一化。这样做的直接好处就是在AC自动机上动规时转移比较方便。
五、拓展:Fail树
例1:[TJOI2013] 单词 http://www.lydsy.com/JudgeOnline/problem.php?id=3172
刚学会AC自动机时,我很兴奋,马上到网上找了一道题来做(就是这道题)。当我兴奋地打完、上交,然后就TLE了……QAQ
这个题的朴素算法就是建立所有串的AC自动机,然后每个串上去跑一遍,然后统计答案。当然是TLE的。正解是用Fail树,利用树的性质来减少重复的计算。
首先根据AC自动机中失配指针的定义,每一个结点有且仅有一个失配指针指向另一结点,即所有结点出度为1。而对于某一结点u,它一定是由它的前趋结点或者失配指针指向u的结点转移过来的。那么考虑把所有的失配指针反向,然后把反向后的失配指针当作边,我们就得到了一棵以结点0为根的树(因为所有结点入度为1)。这样利用树的性质,我们dfs一遍就可以求出答案了。
解法:设结点i出现在cnt[i]个单词中,插入单词时所有经过的结点cnt[u]++,构造出Fail树后dfs一遍,答案是单词末结点子树的cnt值总和。代码如下:
1 #include <set> 2 #include <map> 3 #include <queue> 4 #include <ctime> 5 #include <cmath> 6 #include <cstdio> 7 #include <vector> 8 #include <string> 9 #include <cctype> 10 #include <bitset> 11 #include <cstring> 12 #include <cstdlib> 13 #include <utility> 14 #include <iostream> 15 #include <algorithm> 16 #define lowbit(x) (x)&(-x) 17 #define REP(i,a,b) for(int i=(a);i<=(b);i++) 18 #define PER(i,a,b) for(int i=(a);i>=(b);i--) 19 #define RVC(i,S) for(int i=0;i<(S).size();i++) 20 using namespace std; 21 typedef long long LL; 22 typedef pair<int,int> pii; 23 24 template<class T> inline 25 void read(T& num) { 26 bool start=false,neg=false; 27 char c; 28 num=0; 29 while((c=getchar())!=EOF) { 30 if(c=='-') start=neg=true; 31 else if(c>='0' && c<='9') { 32 start=true; 33 num=num*10+c-'0'; 34 } else if(start) break; 35 } 36 if(neg) num=-num; 37 } 38 /*============ Header Template ============*/ 39 40 const int maxn=(int)(1e6)+5; 41 const int sigma_size=26; 42 vector<int> G[maxn]; 43 queue<int> Q; 44 int ch[maxn][sigma_size]; 45 int f[maxn]; 46 int cnt[maxn],idx[205]; 47 int siz; 48 49 int ord(char c) { 50 return c-'a'; 51 } 52 int insert(char* s) { 53 int u=0,n=strlen(s); 54 for(int i=0; i<n; i++) { 55 int c=ord(s[i]); 56 if(!ch[u][c]) ch[u][c]=++siz; 57 u=ch[u][c]; 58 cnt[u]++; 59 } 60 return u; 61 } 62 63 void getFail() { 64 REP(i,0,siz) G[i].clear(); 65 f[0]=0; 66 for(int c=0; c<sigma_size; c++) { 67 int u=ch[0][c]; 68 if(!u) continue; 69 f[u]=0; 70 G[0].push_back(u); 71 Q.push(u); 72 } 73 while(!Q.empty()) { 74 int k=Q.front(); 75 Q.pop(); 76 for(int c=0; c<sigma_size; c++) { 77 int u=ch[k][c]; 78 if(!u) continue; 79 Q.push(u); 80 int v=f[k]; 81 while(v && !ch[v][c]) v=f[v]; 82 f[u]=ch[v][c]; 83 G[f[u]].push_back(u); 84 } 85 } 86 } 87 88 void dfs(int u,int fa) { 89 RVC(i,G[u]) { 90 int v=G[u][i]; 91 if(v==fa) continue; 92 dfs(v,u); 93 cnt[u]+=cnt[v]; 94 } 95 } 96 97 char buf[maxn]; 98 int main() { 99 siz=0; 100 memset(ch,0,sizeof(ch)); 101 memset(cnt,0,sizeof(cnt)); 102 int n; 103 read(n); 104 REP(i,1,n) { 105 scanf("%s",buf); 106 idx[i]=insert(buf); 107 } 108 getFail(); 109 dfs(0,-1); 110 REP(i,1,n) printf("%d ",cnt[idx[i]]); 111 return 0; 112 }
2015/11/22