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……