简介:
用于多模式串的在一个长文本串上的匹配问题。简单的说就是将KMP算法搬到了Trie树上。所以学习AC自动机前要由Trie树与KMP算法做前置知识。
主要步骤:
1、将所有模式串建成一个Trie树。
2、对Trie树的每个节点都构造失配指针。
AC自动机中失配指针(nxt数组)的运用与KMP算法几乎一模一样,即若能匹配下一位的话,当前的失配指针就向下转移一位;否则,当前的失配指针就要往回走一下,知道下一位能匹配上或失配指针不能再往回走为止。在实际应用中,对失配指针的额外操作会因题目的不同而有所差异。
对于建立Trie树,可以看这篇博客。下面讲讲怎么建nxt数组。
nxt[u]表示当主串s匹配到u节点时,设主串已经匹配到了第i位,若主串的下一位不能继续匹配,即u节点没有表示字符s[i+1]的边时,满足表示的字符串是u最长后缀的新的u节点(因为要保证u表示的字符串与s以第i位为结尾的长度为lu的子串匹配(lu为u节点表示的字符串的长度),因为一开始u就是最长的能匹配的某字符串的前缀,若想不漏掉所有情况地改变u且仍满足前缀与以s[i]结尾后缀的匹配关系,只能把u跳到是u表示的字符串的最长后缀的新的u)。
对于nxt[u],设v一开始是u的父亲。看下v的失配指针对应的点nxt[v]是否有与从u的父亲到u的一样的边(即u的父亲和nxt[v]能否直接匹配下一位),若有,则nxt[u]就是nxt[v]通过那条边所到达的节点;否则将v变为nxt[v],再看下v的失配指针对应的点nxt[v]是否有与从u的父亲到u的一样的边……。为了方便,可以建一个节点0,节点0的表示每个字符的边都存在且指向u,那么v最差就是变为0,此时v一定会存在与从u的父亲到u的一样的边,且这条边指向节点1(表示空串),nxt[u]=1。
实际写代码时,有个优化:如果当前的Trie树节点x表示某个字符num的边不存在,即tree[x][num]==0(建trie树时若某个节点的某条边的指针为0,就说明还没有进行赋值(因为建Trie树时节点从1开始计数;全局数组初始化默认为0)),就让tree[x][num]=tree[nxt[x][num]。即让它保存当v变为x时v仍没有从u的父亲到u的一样的边,v还要再等于nxt[v]…直到有从u的父亲到u的一样的边后返回的结果。这样的话求一个点y的儿子z的失配指针nxt[z]时,设从y到z的边为num,直接nxt[z]=tree[nxt[y]][num]就好了。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 queue<int> q; 2 3 inline void bfs()//因为nxt[x]的深度一定比x的深度小,所以建nxt数组时可用bfs 4 { 5 q.push(1); 6 int head; 7 while(!q.empty()) 8 { 9 head=q.front(); 10 q.pop(); 11 for(int i=0;i<26;++i) 12 { 13 if(!tree[head][i])//如上文的优化 14 tree[head][i]=tree[nxt[head]][i]; 15 else 16 { 17 q.push(tree[head][i]); 18 nxt[tree[head][i]]=tree[nxt[head]][i]; 19 } 20 } 21 } 22 }
再讲一下查询。三种询问方式对应着三种查询(这也是AC自动机的应用)
1、查询有多少模式串在文本串中出现过(例题):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 inline void insert(char *a)//建立Tire树。a为要插入的模式串 2 { 3 int l=strlen(a+1),now=1,num; 4 for(int i=1;i<=l;++i) 5 { 6 num=a[i]-'a'; 7 if(!tree[now][num]) 8 { 9 tree[now][num]=++cnt; 10 //memset(tree[cnt],0,sizeof tree[cnt]);//多组数据时要先清零 11 } 12 now=tree[now][num]; 13 } 14 ed[now]++;//* 15 } 16 17 inline void fin(char *a)//查询过程。a为文本串 18 { 19 int l=strlen(a+1),now=1,num; 20 for(int i=1;i<=l;++i) 21 { 22 num=a[i]-'a'; 23 now=tree[now][num]; 24 for(int k=now;k>1&&ed[k]!=-1;k=nxt[k]) 25 ans+=ed[k],ed[k]=-1;//** 26 } 27 }
每匹配到一个节点u,都要顺着从u开始的nxt指针看一下,防止漏掉长度短的字符串。
讲一下标上**的那一行:对于匹配到的Trie树上的节点,查询的时候nxt数组肯定建完了。因为只要求是否出现的,那么若当前匹配到的Trie上的节点是u,那么下次再看到u时是不会对答案再产生变化了,故不看。
时间复杂度O(n+m)(n为文本串的长度。m为**语句执行的次数,且最多不超过Trie树的节点数),碾压KMP。
(蓝书中的实现代码的复杂度高达O(n*m),m为所有节点的平均深度。连洛谷模板题都跑不过,还是背上面的代码吧)
2、查询模式串在文本串中出现过的次数(例题):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 inline void insert(char *a,int k)//要插入的字符串为a,它的编号为k 2 { 3 int l=strlen(a+1),now=1,num; 4 for(int i=1;i<=l;++i) 5 { 6 num=a[i]-'a'; 7 if(!tree[now][num]) 8 tree[now][num]=++cnt; 9 now=tree[now][num]; 10 } 11 ed[now].push_back(k);//可能有相同的字符串,也要记录。 12 } 13 14 inline void fin(char *a)//a为要查询的文本串 15 { 16 int l=strlen(a+1),now=1,num,k; 17 for(int i=1;i<=l;++i) 18 { 19 num=a[i]-'a'; 20 now=tree[now][num]; 21 for(int k=now;k>=1;k=nxt[k]) 22 { 23 if(ed[k].size()) 24 { 25 int ll=ed[k].size(); 26 for(int j=0;j<ll;++j) 27 tot[ed[k][j]]++; 28 } 29 } 30 } 31 }
这个没什么好说的,时间复杂度为O(n*m),m为所有节点的平均深度。如果用KMP做的话复杂度为O(n*k),k为模式串个数。这样看的话两种算法各有优劣,模式串个数多就用AC自动机,字符串都很长的话就用KMP。
3、查询模式串在文本串中出现过的次数(强化版)(例题):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<queue> 5 6 using namespace std; 7 8 const int LEN=2e5+5,L=2e6+5,N=2e5+5; 9 10 int n,tree[LEN][26],cnt=1,nxt[LEN],tot[N],has[LEN],siz[LEN],lst[LEN],to[LEN],enxt[LEN],ecnt; 11 12 vector<int> ed[LEN]; 13 14 char s[L]; 15 16 inline void insert(char *a,int k) 17 { 18 int l=strlen(a+1),now=1,num; 19 for(int i=1;i<=l;++i) 20 { 21 num=a[i]-'a'; 22 if(!tree[now][num]) 23 tree[now][num]=++cnt; 24 now=tree[now][num]; 25 } 26 ed[now].push_back(k); 27 } 28 29 queue<int> q; 30 31 inline void bfs() 32 { 33 q.push(1); 34 int head; 35 while(!q.empty()) 36 { 37 head=q.front(); 38 q.pop(); 39 for(int i=0;i<26;++i) 40 { 41 if(!tree[head][i]) 42 tree[head][i]=tree[nxt[head]][i]; 43 else 44 { 45 q.push(tree[head][i]); 46 nxt[tree[head][i]]=tree[nxt[head]][i]; 47 } 48 } 49 } 50 } 51 52 inline void addedge(int u,int v) 53 { 54 enxt[++ecnt]=lst[u]; 55 lst[u]=ecnt; 56 to[ecnt]=v; 57 } 58 59 void dfs(int u) 60 { 61 for(int e=lst[u];e;e=enxt[e]) 62 { 63 dfs(to[e]); 64 siz[u]+=siz[to[e]]; 65 } 66 int k=ed[u].size(); 67 for(int i=0;i<k;++i) 68 tot[ed[u][i]]=siz[u]; 69 } 70 71 inline void fin(char *a) 72 { 73 int l=strlen(a+1),now=1,num,k; 74 for(int i=1;i<=l;++i) 75 { 76 num=a[i]-'a'; 77 now=tree[now][num]; 78 siz[now]++; 79 } 80 } 81 82 int main() 83 { 84 for(int i=0;i<26;++i) 85 tree[0][i]=1; 86 scanf("%d",&n); 87 for(int i=1;i<=n;++i) 88 { 89 scanf("%s",s+1); 90 insert(s,i); 91 } 92 bfs(); 93 for(int i=1;i<=cnt;++i) 94 addedge(nxt[i],i); 95 scanf("%s",s+1); 96 fin(s); 97 dfs(1); 98 for(int i=1;i<=n;++i) 99 printf("%d ",tot[i]); 100 return 0; 101 }
强化版更适合看完整的代码。插入和建Trie树与原版一样。多了一个求子树和的dfs,fin查询函数更简单了。
我们发现原版的复杂度真是不尽人意,堂堂的省选知识AC自动机竟和普及组就学的KMP五五开,这怎么行?强化版用了一个建nxt树的思路。考虑每个节点的贡献来源,要么是当前节点被匹配到一次,使多了一个贡献,要么是某个节点被匹配到一次,通过nxt恰好能走到当前节点,于是又让当前节点的贡献加1。对于每个Trie树上的节点,发现它有且只有一个nxt(不考虑节点0)。那么从一个只有Trie树上的节点(不包含节点0)的新图上,将每个点的nxt向相应的点连一条边,一定会生成一个根为1的树。若每个点的权值为它被匹配到的次数的话,发现每个点对答案的贡献正是以它为根的子树的和。故可以通过建nxt树、最后求子树和的方式快速求出每个串的出现次数。
(想不出来就要换的角度嘛。一个个找相应串加不行的话,看看贡献的来源与去向,搞个整体收集不就行了?)
时间复杂度O(n+m),m为Trie树的节点个数(又一次碾压了KMP,看来AC自动机要处处碾压KMP了)