begin:2019/5/2html
update 2020/6/12 更新了LaTeX(咕了很久c++
感谢你们支持!数组
AC自动机真是个好东西!以前学\(KMP\)被\(Next\)指针搞晕了,因此咕了许久都不敢开AC自动机,近期学完以后,发现AC自动机并非很难,特别是对于\(KMP\),我的感受AC自动机比\(KMP\)要好理解一些,多是由于我对树上的东西比较敏感(实际是由于我到如今都不会\(KMP\))。学习
不少人都说AC自动机是在\(Trie\)树上做\(KMP\),我不否定这一种观点,由于这确实是这样,不过对于刚开始学AC自动机的同窗们就一些误导性的理解(至少对我是这样的)。\(KMP\)是创建在一个字符串上的,如今把\(KMP\)搬到了树上,不是很麻烦吗?实际上AC自动机只是有\(KMP\)的一种思想,实际上跟一个字符串的\(KMP\)有着很大的不一样。优化
因此看这篇blog,请放下\(KMP\),理解好\(Trie\),再来学习。spa
1.\(Trie\)(很重要哦)指针
2.\(KMP\)的思想(懂思想就能够了,不须要很熟练)code
给定\(n\)个模式串和\(1\)个文本串,求有多少个模式串在文本串里出现过。htm
注意:是出现过,就是出现屡次只算一次。blog
默认这里每个人都已经会了\(Trie\)。
咱们将\(n\)个模式串建成一颗\(Trie\)树,建树的方式和建\(Trie\)彻底同样。
假如咱们如今有文本串\(ABCDBC\)。
咱们用文本串在\(Trie\)上匹配,刚开始会通过\(二、三、4\)号点,发现到\(4\),成功地匹配了一个模式串,而后就不能再继续匹配了,这时咱们还要从新继续从根开始匹配吗?
不,这样的效率太慢了。这时咱们就要借用\(KMP\)的思想,从\(Trie\)上的某个点继续开始匹配。
明显在这颗\(Trie\)上,咱们能够继续从\(7\)号点开始匹配,而后匹配到\(8\)。
那么咱们怎么肯定从那个点开始匹配呢?咱们称\(i\)匹配失败后继续从\(j\)开始匹配,\(j\)是\(i\)的\(Fail\)(失配指针)。
\(Fail\)指针的实质含义是什么呢?
若是一个点\(i\)的\(Fail\)指针指向\(j\)。那么\(root\)到\(j\)的字符串是\(root\)到\(i\)的字符串的一个后缀。
举个例子:(例子来自上面的图
i:4 j:7 root到i的字符串是“ABC” root到j的字符串是“BC” “BC”是“ABC”的一个后缀 因此i的Fail指针指向j
同时咱们发现,“\(C\)”也是“\(ABC\)”的一个后缀。
因此\(Fail\)指针指的\(j\)的深度要尽可能大。
重申一下\(Fail\)指针的含义:((最长的(当前字符串的后缀))在\(Trie\)上能够查找到)的末尾编号。
感受读起来挺绕口的蛤。感性理解一下就行了,没什么卵用的。知道\(Fail\)有什么用就好了。
首先咱们能够肯定,每个点\(i\)的\(Fail\)指针指向的点的深度必定是比\(i\)小的。(Fail指的是后缀啊)
第一层的\(Fail\)必定指的是\(root\)。(比深度\(1\)还浅的只有\(root\)了)
设点\(i\)的父亲\(fa\)的\(Fail\)指针指的是\(fafail\),那么若是\(fafail\)有和\(i\)值相同的儿子\(j\),那么\(i\)的\(Fail\)就指向\(j\)。这里可能比较难理解一点,建议画图理解,不过等会转换成代码就很好理解了。
因为咱们在处理\(i\)的状况必需要先处理好\(fa\)的状况,因此求\(Fail\)咱们使用\(BFS\)来实现。
一、刚开始咱们不是要初始化第一层的\(fail\)指针为\(root\),其实咱们能够建一个虚节点\(0\)号节点,将\(0\)的全部儿子指向\(root\)(\(root\)编号为\(1\),记得初始化),而后\(root\)的\(fail\)指向\(0\)就OK了。效果是同样的。
二、若是不存在一个节点\(i\),那么咱们能够将那个节点设为\(fafail\)的((值和\(i\)相同)的儿子)。保证存在性,就算是\(0\)也能够成功返回到根,由于\(0\)的全部儿子都是根。
三、不管\(fafail\)存不存在和\(i\)值相同的儿子\(j\),咱们均可以将\(i\)的\(fail\)指向\(j\)。由于在处理\(i\)的时候\(j\)已经处理好了,若是出现这种状况,\(j\)的值是第\(2\)种状况,也是有实际值的,因此没有问题。
四、实现时不记父亲,咱们直接让父亲更新儿子
void getFail(){ for(int i=0;i<26;i++)trie[0].son[i]=1; //初始化0的全部儿子都是1 q.push(1);trie[1].fail=0; //将根压入队列 while(!q.empty()){ int u=q.front();q.pop(); for(int i=0;i<26;i++){ //遍历全部儿子 int v=trie[u].son[i]; //处理u的i儿子的fail,这样就能够不用记父亲了 int Fail=trie[u].fail; //就是fafail,trie[Fail].son[i]就是和v值相同的点 if(!v){trie[u].son[i]=trie[Fail].son[i];continue;} //不存在该节点,第二种状况 trie[v].fail=trie[Fail].son[i]; //第三种状况,直接指就能够了 q.push(v); //存在实节点才压入队列 } } }
求出了\(Fail\)指针,查询就变得十分简单了。
为了不重复计算,咱们每通过一个点就打个标记为\(-1\),下一次通过就不重复计算了。
同时,若是一个字符串匹配成功,那么他的\(Fail\)也确定能够匹配成功(后缀嘛),因而咱们就把\(Fail\)再统计答案,一样,\(Fail\)的\(Fail\)也能够匹配成功,以此类推……通过的点累加\(flag\),标记为\(-1\)。
最后主要仍是和\(Trie\)的查询是同样的。
int query(char* s){ int u=1,ans=0,len=strlen(s); for(int i=0;i<len;i++){ int v=s[i]-'a'; int k=trie[u].son[v]; //跳Fail while(k>1&&trie[k].flag!=-1){ //通过就不统计了 ans+=trie[k].flag,trie[k].flag=-1; //累加上这个位置的模式串个数,标记 已 通过 k=trie[k].fail; //继续跳Fail } u=trie[u].son[v]; //到儿子那,存在性看上面的第二种状况 } return ans; }
#include<bits/stdc++.h> #define maxn 1000001 using namespace std; struct kkk{ int son[26],flag,fail; }trie[maxn]; int n,cnt; char s[1000001]; queue<int >q; void insert(char* s){ int u=1,len=strlen(s); for(int i=0;i<len;i++){ int v=s[i]-'a'; if(!trie[u].son[v])trie[u].son[v]=++cnt; u=trie[u].son[v]; } trie[u].flag++; } void getFail(){ for(int i=0;i<26;i++)trie[0].son[i]=1; //初始化0的全部儿子都是1 q.push(1);trie[1].fail=0; //将根压入队列 while(!q.empty()){ int u=q.front();q.pop(); for(int i=0;i<26;i++){ //遍历全部儿子 int v=trie[u].son[i]; //处理u的i儿子的fail,这样就能够不用记父亲了 int Fail=trie[u].fail; //就是fafail,trie[Fail].son[i]就是和v值相同的点 if(!v){trie[u].son[i]=trie[Fail].son[i];continue;} //不存在该节点,第二种状况 trie[v].fail=trie[Fail].son[i]; //第三种状况,直接指就能够了 q.push(v); //存在实节点才压入队列 } } } int query(char* s){ int u=1,ans=0,len=strlen(s); for(int i=0;i<len;i++){ int v=s[i]-'a'; int k=trie[u].son[v]; //跳Fail while(k>1&&trie[k].flag!=-1){ //通过就不统计了 ans+=trie[k].flag,trie[k].flag=-1; //累加上这个位置的模式串个数,标记已通过 k=trie[k].fail; //继续跳Fail } u=trie[u].son[v]; //到下一个儿子 } return ans; } int main(){ cnt=1; //代码实现细节,编号从1开始 scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%s",s); insert(s); } getFail(); scanf("%s",s); printf("%d\n",query(s)); return 0; }
updata:2019/5/7 AC自动机的应用
先拿P3796 【模板】AC自动机(增强版)来讲吧。
无疑,做为模板2,这道题的解法也是十分的经典。
咱们先来分析一下题目:输入和模板1同样
一、求出现次数最多的次数
二、求出现次数最多的模式串
明显,咱们若是统计出每个模式串在文本串出现的次数,那么这道题就变得十分简单了,那么问题就变成了如何统计每一个模式串出现的次数。
作法:AC自动机
首先题目统计的是出现次数最多的字符串,因此有重复的字符串是没有关系的。(由于后面的会覆盖前面的,统计的答案也是同样的)
那么咱们就将标记模式串的\(flag\)设为当前是第几个模式串。就是下面插入时的变化:
trie[u].flag++; 变为 trie[u].flag=num; //num表示该字符串是第num个输入的
求\(Fail\)指针没有变化,原先怎么求就怎么求。
查询:咱们开一个数组\(vis\),表示第\(i\)个字符串出现的次数。
由于是重复计算,因此不能标记为\(-1\)了。
咱们每通过一个点,若是有模式串标记,就将\(vis[模式串标记]++\)。而后继续跳fail,缘由上面说过了。
这样咱们就能够将每一个模式串的出现次数统计出来。剩下的你们应该都会QwQ!
//AC自动机增强版 #include<bits/stdc++.h> #define maxn 1000001 using namespace std; char s[151][maxn],T[maxn]; int n,cnt,vis[maxn],ans; struct kkk{ int son[26],fail,flag; void clear(){memset(son,0,sizeof(son));fail=flag=0;} }trie[maxn]; queue<int>q; void insert(char* s,int num){ int u=1,len=strlen(s); for(int i=0;i<len;i++){ int v=s[i]-'a'; if(!trie[u].son[v])trie[u].son[v]=++cnt; u=trie[u].son[v]; } trie[u].flag=num; //变化1:标记为第num个出现的字符串 } void getFail(){ for(int i=0;i<26;i++)trie[0].son[i]=1; q.push(1);trie[1].fail=0; while(!q.empty()){ int u=q.front();q.pop(); int Fail=trie[u].fail; for(int i=0;i<26;i++){ int v=trie[u].son[i]; if(!v){trie[u].son[i]=trie[Fail].son[i];continue;} trie[v].fail=trie[Fail].son[i]; q.push(v); } } } void query(char* s){ int u=1,len=strlen(s); for(int i=0;i<len;i++){ int v=s[i]-'a'; int k=trie[u].son[v]; while(k>1){ if(trie[k].flag)vis[trie[k].flag]++; //若是有模式串标记,更新出现次数 k=trie[k].fail; } u=trie[u].son[v]; } } void clear(){ for(int i=0;i<=cnt;i++)trie[i].clear(); for(int i=1;i<=n;i++)vis[i]=0; cnt=1;ans=0; } int main(){ while(1){ scanf("%d",&n);if(!n)break; clear(); for(int i=1;i<=n;i++){ scanf("%s",s[i]); insert(s[i],i); } scanf("%s",T); getFail(); query(T); for(int i=1;i<=n;i++)ans=max(vis[i],ans); //最后统计答案 printf("%d\n",ans); for(int i=1;i<=n;i++) if(vis[i]==ans) printf("%s\n",s[i]); } }
update:2019/5/9
让咱们了分析一下刚才那个模板2的时间复杂度,算了不分析了,直接告诉你吧,这样暴力去跳\(fail\)的最坏时间复杂度是\(O(模式串长度 · 文本串长度)\)。
为何?由于对于每一次跳\(fail\)咱们都只使深度减\(1\),那样深度是多少,每一次跳的时间复杂度就是多少。那么还要乘上文本串长度,就几乎是 \(O(模式串长度 · 文本串长度)\)的了。
那么模板1的时间复杂度为何就只有\(O(模式串总长)\)。由于每个\(Trie\)上的点都只会通过一次(打了标记),但模板2每个点就不止通过一次了(重复算,不打标记),因此时间复杂度就爆炸了。
那么咱们可不可让模板2的\(Trie\)上每一个点只通过一次呢?
嗯~,还真能够!
题目看这里:P5357 【模板】AC自动机(二次增强版)
让咱们把\(Trie\)上的\(fail\)都想象成一条条有向边,那么咱们若是在一个点对那个点进行一些操做,那么沿着这个点连出去的点也会进行操做(就是跳\(fail\)),因此咱们才要暴力跳\(fail\)去更新以后的点。
咱们仍是用上面的图,举个例子解释一下我刚才的意思。
咱们先找到了编号\(4\)这个点,编号\(4\)的\(fail\)连向编号\(7\)这个点,编号\(7\)的\(fail\)连向编号\(9\)这个点。那么咱们要更新编号\(4\)这个点的值,同时也要更新编号\(7\)和编号\(9\),这就是暴力跳\(fail\)的过程。
咱们下一次找到编号\(7\)这个点,还要再次更新编号\(9\),因此时间复杂度就在这里被浪费了。
那么咱们可不能够在找到的点打一个标记,最后再一次性将标记所有上传 来 更新其余点的\(ans\)。例如咱们找到编号\(4\),在编号\(4\)这个点打一个\(ans\)标记为\(1\),下一次找到了编号\(7\),又在编号\(7\)这个点打一个\(ans\)标记为\(1\),那么最后,咱们直接从编号\(4\)开始跳\(fail\),而后将标记\(ans\)上传,((点i的fail)的ans)加上(点i的ans),最后使编号\(4\)的\(ans\)为\(1\),编号\(7\)的\(ans\)为\(2\),编号\(9\)的\(ans\)为\(2\),这样的答案和暴力跳\(fail\)是同样的,而且每个点只通过了一次。
最后咱们将有\(flag\)标记的\(ans\)传到\(vis\)数组里,就求出了答案。
em……,建议先消化一下。
那么如今问题来了,怎么肯定更新顺序呢?明显咱们打了标记后确定是从深度大的点开始更新上去的。
怎么实现呢?拓扑排序!
咱们使每个点向它的\(fail\)指针连一条边,明显,每个点的出度为\(1\)(\(fail\)只有一个),入度可能不少,因此咱们就不须要像拓扑排序那样先建个图了,直接往\(fail\)指针跳就能够了。
最后咱们根据\(fail\)指针建好图后(想象一下,程序里不用实现),必定是一个\(DAG\),具体缘由不解释(很简单的),那么咱们就直接在上面跑拓扑排序,而后更新\(ans\)就能够了。
首先是\(getfail\)这里,记得将\(fail\)的入度\(in\)更新。
trie[v].fail=trie[Fail].son[i]; in[trie[v].fail]++; //记得加上入度
而后是\(query\),不用暴力跳\(fail\)了,直接打上标记就好了,很简单吧
void query(char* s){ int u=1,len=strlen(s); for(int i=0;i<len;++i) u=trie[u].son[s[i]-'a'],trie[u].ans++; //直接打上标记 }
最后是拓扑,解释都在注释里了OwO!
void topu(){ for(int i=1;i<=cnt;++i) if(in[i]==0)q.push(i); //将入度为0的点所有压入队列里 while(!q.empty()){ int u=q.front();q.pop();vis[trie[u].flag]=trie[u].ans; //若是有flag标记就更新vis数组 int v=trie[u].fail;in[v]--; //将惟一连出去的出边fail的入度减去(拓扑排序的操做) trie[v].ans+=trie[u].ans; //更新fail的ans值 if(in[v]==0)q.push(v); //拓扑排序常规操做 } }
应该仍是很好理解的吧,实现起来也没有多难嘛!
对了还有重复单词的问题,和下面讲的"P3966[TJOI2013]单词"的解决方法同样的,不讲了吧。
这道题和上面那道题没有什么不一样,文本串就是将模式串用神奇的字符(例如"♂")隔起来的串。
但这道题有相同字符串要统计,因此咱们用一个\(Map\)数组存这个字符串指的是\(Trie\)中的那个位置,最后把\(vis[Map[i]]\)输出就OK了。
下面是P5357【模板】AC自动机(二次增强版)的代码(套娃?大雾),剩下的你们怎么改应该仍是知道的吧。
#include<bits/stdc++.h> #define maxn 2000001 using namespace std; char s[maxn],T[maxn]; int n,cnt,vis[200051],ans,in[maxn],Map[maxn]; struct kkk{ int son[26],fail,flag,ans; }trie[maxn]; queue<int>q; void insert(char* s,int num){ int u=1,len=strlen(s); for(int i=0;i<len;++i){ int v=s[i]-'a'; if(!trie[u].son[v])trie[u].son[v]=++cnt; u=trie[u].son[v]; } if(!trie[u].flag)trie[u].flag=num; Map[num]=trie[u].flag; } void getFail(){ for(int i=0;i<26;i++)trie[0].son[i]=1; q.push(1); while(!q.empty()){ int u=q.front();q.pop(); int Fail=trie[u].fail; for(int i=0;i<26;++i){ int v=trie[u].son[i]; if(!v){trie[u].son[i]=trie[Fail].son[i];continue;} trie[v].fail=trie[Fail].son[i]; in[trie[v].fail]++; q.push(v); } } } void topu(){ for(int i=1;i<=cnt;++i) if(in[i]==0)q.push(i); //将入度为0的点所有压入队列里 while(!q.empty()){ int u=q.front();q.pop();vis[trie[u].flag]=trie[u].ans; //若是有flag标记就更新vis数组 int v=trie[u].fail;in[v]--; //将惟一连出去的出边fail的入度减去(拓扑排序的操做) trie[v].ans+=trie[u].ans; //更新fail的ans值 if(in[v]==0)q.push(v); //拓扑排序常规操做 } } void query(char* s){ int u=1,len=strlen(s); for(int i=0;i<len;++i) u=trie[u].son[s[i]-'a'],trie[u].ans++; } int main(){ scanf("%d",&n); cnt=1; for(int i=1;i<=n;++i){ scanf("%s",s); insert(s,i); }getFail();scanf("%s",T); query(T);topu(); for(int i=1;i<=n;++i)printf("%d\n",vis[Map[i]]); }
To be continue……