模板题:HDU2222
AC自动机将KMP算法与trie树(字典树)相互结合
其主要处理多模匹配(即给定n个单词,再加一篇文章母串,在母串中查找这些单词)
如果是KMP算法,就会让每个字串与母串进行匹配,时间复杂度太高,所以用AC自动机。
在AC自动机算法中,主要步骤为:
1:构建字典树;
2:对每个结点建立失配指针;
3:模式匹配;
而失配指针则是AC自动机的关键所在
失配指针指向的节点代表的字符串是当前节点代表的字符串的后缀。
所以,当已经知道某个节点的失配指针所指向的节点时,那么其字节点的失配指针所指向的则为与该节点代表字符相同的,该节点父亲节点失配指针所指向的节点的字节点。(有点绕)
即:对于跳转位置的选择,基于以下两点:
(1)对于根节点的所有子节点,它们的 fail 指针都指向根节点;
(2)对于其它节点,不妨设该节点为 u(对应字符为“ch”),沿着它的父节点
的 fail指针走,直到走到一个节点 v,其对应字符也为“ch”,然后把节点 u 的
fail 指针指向节点 v。如果一直走到根节点都没找到,则把 fail 指针指向根
点。
显然这个 v 节点的深度是小于 u 节点的,因此我们可以通过按节点的深度大小,也就是
BFS 的顺序来构建失配指针,构建过程与 KMP 类似,KMP 中 i 的 next 是沿着 i-1 的 next 不停往前跳来求,而 AC 自动机中 u 的 fail 则通过父节点的 fail 不停往上跳,直到找到一个节点它拥有对应字符的转移边为止来求得。
1:字典树
1 void insert(char *s) //构建 Trie 树 2 { 3 int u=0; 4 int n=strlen(s); 5 for(int i=0;i<n;i++) 6 { 7 int c=s[i]-'a'; //把 a..z 转为 0..25 8 if(!trie[u][c]) 9 { 10 memset(trie[tot],0,sizeof(trie[tot])); 11 val[tot]=0; 12 trie[u][c]=tot++; 13 } 14 u=trie[u][c]; 15 } 16 val[u]++; //val[u]表示节点 0~节点 u 构成的单词数量 17 }
2:失配指针
1 void getfail() 2 { 3 queue<int> q; 4 fail[0]=0; 5 int u=0; 6 for(int i=0;i<26;i++) 7 { 8 u=trie[0][i]; 9 if(u) 10 { 11 q.push(u); 12 fail[u]=0; 13 last[u]=0; 14 } 15 }//最先初始,即根节点的字节点的失配指针都指向根 16 while(!q.empty()) 17 { 18 int r=q.front();q.pop(); 19 for(int i=0;i<26;i++) 20 { 21 u=trie[r][i]; 22 if(!u)//不存在,则跳向其父节点 r 前缀指针指向的第 i 个子节点 23 { 24 trie[r][i]=trie[fail[r]][i]; 25 continue; 26 } 27 q.push(u); 28 int v=fail[r]; 29 while(v&&!trie[v][i]) v=fail[v];//v存在并且 v没有第i个子节点,则跳到 v的前缀指针指向的节点 30 fail[u]=trie[v][i]; 31 last[u]=val[fail[u]]?fail[u]:last[fail[u]]; 32 } 33 } 34 }
3:模式匹配
1 int find(char *s) 2 { 3 int u=0,cnt=0; 4 int lens=strlen(s); 5 for(int i=0;i<lens;i++) 6 { 7 int c=s[i]-'a'; 8 u=trie[u][c]; 9 int temp=0; 10 if(val[u]) //如果u是一个完整单词 11 temp=u; 12 else if(last[u]) 13 temp=last[u]; 14 while(temp) 15 { 16 cnt+=val[temp]; 17 val[temp]=0; 18 temp=last[temp]; 19 } 20 } 21 return cnt; 22 }
最终代码:
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int maxn=1000010; 4 const int maxm=50*10010; 5 char t[60],s[maxn]; 6 int n; 7 int trie[maxm][26]; 8 int val[maxm];//每个字符串的结尾节点都有一个非零的ed 9 int fail[maxm];//失配指针 10 int last[maxm];//last[i]=j,j节点表示的单词是i节点单词的后缀,且j节点是单词节点 11 int tot; 12 void insert(char *s) //构建 Trie 树 13 { 14 int u=0; 15 int n=strlen(s); 16 for(int i=0;i<n;i++) 17 { 18 int c=s[i]-'a'; //把 a..z 转为 0..25 19 if(!trie[u][c]) 20 { 21 memset(trie[tot],0,sizeof(trie[tot])); 22 val[tot]=0; 23 trie[u][c]=tot++; 24 } 25 u=trie[u][c]; 26 } 27 val[u]++; //val[u]表示节点 0~节点 u 构成的单词数量 28 } 29 void getfail() 30 { 31 queue<int> q; 32 fail[0]=0; 33 int u=0; 34 for(int i=0;i<26;i++) 35 { 36 u=trie[0][i]; 37 if(u) 38 { 39 q.push(u); 40 fail[u]=0; 41 last[u]=0; 42 } 43 }//最先初始,即根节点的字节点的失配指针都指向根 44 while(!q.empty()) 45 { 46 int r=q.front();q.pop(); 47 for(int i=0;i<26;i++) 48 { 49 u=trie[r][i]; 50 if(!u)//不存在,则跳向其父节点 r 前缀指针指向的第 i 个子节点 51 { 52 trie[r][i]=trie[fail[r]][i]; 53 continue; 54 } 55 q.push(u); 56 int v=fail[r]; 57 while(v&&!trie[v][i]) v=fail[v];//v存在并且 v没有第i个子节点,则跳到 v的前缀指针指向的节点 58 fail[u]=trie[v][i]; 59 last[u]=val[fail[u]]?fail[u]:last[fail[u]]; 60 } 61 } 62 } 63 int find(char *s) 64 { 65 int u=0,cnt=0; 66 int lens=strlen(s); 67 for(int i=0;i<lens;i++) 68 { 69 int c=s[i]-'a'; 70 u=trie[u][c]; 71 int temp=0; 72 if(val[u]) //如果u是一个完整单词 73 temp=u; 74 else if(last[u]) 75 temp=last[u]; 76 while(temp) 77 { 78 cnt+=val[temp]; 79 val[temp]=0; 80 temp=last[temp]; 81 } 82 } 83 return cnt; 84 } 85 int main() 86 { 87 int T; 88 cin>>T; 89 while(T--) 90 { 91 memset(trie[0],0,sizeof(trie[0]));//初始化0节点所对的子节点 92 memset(last,0,sizeof(last)); 93 tot=1; 94 cin>>n; 95 for(int i=1;i<=n;i++) 96 { 97 scanf("%s",&t); 98 insert(t); 99 } 100 getfail(); 101 scanf("%s",s); 102 int ans=find(s); 103 cout<<ans<<endl; 104 } 105 return 0; 106 }