Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法。
解决什么问题呢?
KMP是给你一个模式串和一个文本串,要求求出模式串的匹配位置。
而AC自动机是给你一个文本串和一堆模式串,问你能匹配上多少模式串。
容易想到我们可以在Trie上跑KMP,结合二者的优点就是我们的AC自动机了。
想学AC自动机,先要明白Trie树的实现方法和KMP的思想。
AC自动机大概就长这个样,它是在字典树的基础上多了个失配指针,告诉你失配了该往哪里跑,就相当于KMP中的next数组。
比如我们在K处失配
显然next记录的位置是在k的父亲的next下的s[k]的编号
我们的next是按照从根节点开始的BFS序求解,就是这样。
1 void get_next() 2 { 3 for(int i=0;i<26;i++) 4 { 5 if(trie[0][i]) 6 { 7 next[trie[0][i]]=0; 8 q.push(trie[0][i]); 9 //因为按照BFS序找next嘛,可以理解为什么按BFS序吧 10 } 11 }//先预处理深度为1的点的next,显然他父亲没有与i相同的兄弟,所以直接赋0 12 while(!q.empty()) 13 { 14 int cnt=q.front(); q.pop();//取队首,找他儿子的next 15 for(int i=0;i<26;i++) 16 { 17 if(trie[cnt][i]) 18 { 19 next[trie[cnt][i]]=trie[next[cnt]][i];//显然的转移 20 q.push(trie[cnt][i]); 21 } 22 else 23 trie[cnt][i]=trie[next[cnt]][i];//既然不存在,我们查找它时就应该找到上面那个i的位置 24 //由于是按BFS序找的,所以 trie[next[cnt]][i]一定找过 25 } 26 } 27 }
这就是next的求法。
对了,忘了说了,建AC自动机的方法就是建一颗Trie:
1 void _insert(string s) 2 { 3 int now=0; 4 int size=s.size(); 5 for(int i=0;i<size;i++) 6 { 7 int x=s[i]-'a'; 8 if(!trie[now][x]) 9 trie[now][x]=++total; 10 now=trie[now][x]; 11 } 12 _word[now]++;//我们令_word记录以now结尾有几个单词,用于解题的。 13 }
如果看官实在看不懂鄙人丑陋的码风,可以转开始的链接-Trie
好,接下来就是查找了:
给我们一个文本串S,问在AC自动机上有多少单词出现在了S上(多模式串匹配)
我们的_word还是很有用的,就是我们每找到一个结尾k,那么就可以保证结尾为k的所有单词都被访问过一遍,所以我们的答案加上_word[k],之后为避免重复,将_word[k]清零。
查找嘛,正常情况就按照查找字典树的办法查找,失配的话就跳到next,同时我们要保证你找到的一定是单词结尾(显然若不存在,程序结束)。
1 int find(string s) 2 { 3 int size=s.size(); 4 int now=0,ans=0; 5 for(int i=0;i<size;i++) 6 { 7 int x=s[i]-'a'; 8 now=trie[now][x]; 9 for(int k=now;k&&_word[k];k=next[k]) 10 { 11 ans+=_word[k]; 12 _word[k]=0; 13 } 14 } 15 return ans; 16 }
于是就结束了。
//洛谷3808:
#include<iostream> #include<cstdio> #include<algorithm> #include<string> #include<queue> using namespace std; int n,trie[1000010][30],next[1000010],total,_word[10000010]; queue<int>q; void _insert(string s) { int now=0; int size=s.size(); for(int i=0;i<size;i++) { int x=s[i]-'a'; if(!trie[now][x]) trie[now][x]=++total; now=trie[now][x]; } _word[now]++; } void get_next() { for(int i=0;i<26;i++) { if(trie[0][i]) { next[trie[0][i]]=0; q.push(trie[0][i]); } } while(!q.empty()) { int cnt=q.front(); q.pop(); for(int i=0;i<26;i++) { if(trie[cnt][i]) { next[trie[cnt][i]]=trie[next[cnt]][i]; q.push(trie[cnt][i]); } else trie[cnt][i]=trie[next[cnt]][i]; } } } int find(string s) { int size=s.size(); int now=0,ans=0; for(int i=0;i<size;i++) { int x=s[i]-'a'; now=trie[now][x]; for(int k=now;k&&-(_word[k]+1);k=next[k])//“k&&_word[k]”走到头再找next ,不卡时间过不去 { ans+=_word[k]; _word[k]=-1; } } return ans; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { string s; cin>>s; _insert(s); } get_next(); string s; cin>>s; cout<<find(s); }
进阶版:
不同的是,他让你求哪个串出现的次数最多,而且可能有许多串出现的次数一样多。
因为输入的特殊性,我们在输入时赋予每个串的结尾一个映射belong[i],记录这个串是第几个输入进去的(显然纵然有重复模式串也不影响答案)
然后对于每次查找,我们另数组_time[k]表示k这个字符串出现了几次,在查找时每找到一个点k,我们令_time[belong[k]]++.
最后顺序统计答案。
1 #include<iostream> 2 #include<cstdio> 3 #include<algorithm> 4 #include<map> 5 #include<string> 6 #include<queue> 7 #include<cstring> 8 using namespace std; 9 int trie[100010][30],next[100010],n,t,total,_time[100010],belong[100010]; 10 queue<int>q; 11 string s[100010]; 12 void _insert(string s1,int j) 13 { 14 int size=s1.size(),now=0; 15 for(int i=0;i<size;i++) 16 { 17 int x=s1[i]-'a'; 18 if(!trie[now][x]) 19 trie[now][x]=++total; 20 now=trie[now][x]; 21 } 22 belong[now]=j; 23 } 24 void get_next() 25 { 26 for(int i=0;i<26;i++) 27 if(trie[0][i]) 28 q.push(trie[0][i]); 29 while(!q.empty()) 30 { 31 int cnt=q.front(); q.pop(); 32 for(int i=0;i<26;i++) 33 { 34 if(trie[cnt][i]) 35 { 36 next[trie[cnt][i]]=trie[next[cnt]][i]; 37 q.push(trie[cnt][i]); 38 } 39 else 40 trie[cnt][i]=trie[next[cnt]][i]; 41 } 42 } 43 } 44 int find(string s1) 45 { 46 int size=s1.size(); 47 int now=0,ans=0; 48 for(int i=0;i<size;i++) 49 { 50 int x=s1[i]-'a'; 51 now=trie[now][x]; 52 for(int k=now;k;k=next[k]) 53 _time[belong[k]]++; 54 } 55 for(int i=1;i<=n;i++) 56 ans=max(ans,_time[i]); 57 return ans; 58 } 59 int main() 60 { 61 while(scanf("%d",&n)!=EOF) 62 { 63 if(n==0)break; 64 memset(next,0,sizeof(next)); 65 memset(_time,0,sizeof(_time)); 66 memset(trie,0,sizeof(trie)); 67 memset(belong,0,sizeof(belong)); 68 total=0; 69 for(int i=1;i<=n;i++) 70 { 71 cin>>s[i]; 72 _insert(s[i],i); 73 } 74 get_next(); 75 string s1; 76 cin>>s1; 77 int num=find(s1); 78 printf("%d ",num); 79 for(int i=1;i<=n;i++) 80 { 81 if(_time[i]==num) 82 cout<<s[i]<<endl; 83 } 84 } 85 }
AC自动机是一种优秀的多模式串匹配算法,应用广泛,希望大家能深入理解。