【AC自動機】【字符串】【字典樹】AC自動機 學習筆記
blog:www.wjyyy.top
AC自動機是一種毒瘤的方便的多模式串匹配算法。基於字典樹,用到了類似KMP的思維。
AC自動機與KMP不同的是,AC自動機可以同時匹配多個模式串,而復雜度不會達到太高。如果用KMP多次匹配字符串,復雜度就是\(O(k(n+m))\)。
我們知道,如果讓一個字符串頭對頭或者完全匹配其他字符串,用字典樹來匹配是最為方便的。但是如果匹配過程中發現當前節點沒有目標兒子,就發生了失配。在KMP字符串匹配中,失配可以跳到給當前位置預處理出的nxt,繼續匹配。
而AC自動機在字典樹上,我們如何找出每個節點失配位置呢?我們知道,像KMP一樣,失配位置是唯一確定的。而在字典樹上,一條路徑唯一對應了一個子串,因此也是唯一確定的。
KMP中的nxt數組是由變量j承接了前一個位置的nxt,我們考慮在AC自動機中也讓失配指針從父節點轉移過來。那麽如此一來,當前節點(設為‘c‘)的失配指針就會從當前父節點的失配指針一直沿失配指針遞歸,找到第一個有以‘c‘字符為兒子的節點,把當前節點的失配指針連接到這個節點的‘c‘兒子上。如此做下去,會發現有了失配指針的樹變成了一個圖。但是如果上面的回溯過程找到根了還沒有找到怎麽辦?
正常情況下,字典樹的根是不帶任何字符的,也就是說它是一個空節點,也是重新匹配的開始。如果我們一開始匹配就出現了失誤,也就是根節點都沒有這個兒子,我們當然要留在自己位置上繼續做,因此根節點的失配指針指向自己(同時防止越界)。同理,根節點的兒子們失配指針指向根節點,因為在這裏失配了,接下來只有兩種情況:一是根節點也沒有這個兒子,於是回歸到根節點的一般情況;二是根節點有這個兒子,根節點有這個兒子我們就通過當前節點的失配指針先走到根節點,再走到這個兒子去。
於是,我們的Trie樹變成了Trie圖。
可以根據上面的前兩幅圖更清楚的了解AC自動機,其中紅色邊是fail邊。我們可以發現一個有趣的事情,fail指針可以構成一棵有向樹,註意到每條單獨的鏈都沒有分支,而且一條鏈上的字母總是一致的,因此可能在以後的題目或者優化中出現。(就像KMP的nxt一樣)
實際上在構建AC自動機時,我們的失配指針並不這樣建。為了減小常數(也許是這個原因),我們認為當前節點如果沒有兒子‘c‘,就把當前節點代表‘c‘兒子的指針連向當前節點失配指針的‘c‘兒子。因為一個點的失配指針指向的節點總是比這個點淺,所以我們用BFS來做,深度較淺的點總比深度深的點先被訪問,也因此,當前節點的失配指針的‘c‘兒子一定有位置,即使不是它真正的兒子,也一定是它通過失配指針索引得到的。在最壞的情況下,如果失配指針回溯的過程中怎麽也找不到這樣的兒子,自然而然當前節點的‘c‘兒子就連向根了。
與字典樹類似,AC自動機成功匹配就是找到了一個單詞的結尾,我們在構建字典樹時就應該把每個模式串的結尾做上標記。但是如果兩個模式串有包含關系怎麽辦?有兩種方法可以完成,一是訪問到每個節點時暴力跳fail指針,直到遞歸到根,對答案的貢獻就是這條路徑的標記數;二是構建fail樹,跳就是沿著fail樹在跳,只需要預處理出fail樹上每個節點到根路徑上標記的數目(前綴和),就可以在當前節點記錄答案。看上去第二種方法復雜度更優,但是它有局限性。也就是當確切地統計每個模式串出現的次數時,這種直接用fail樹統計出現次數和的方法不能適用。
Code of luoguP3796:
這個題要註意重復的模式串統計問題
#include<cstdio> #include<cstring> #include<vector> using std::vector; vector<int> same[155];//與某一個模式串相同的模式串編號 struct node { int End,num;//num表示相同模式串個數,End表示是否為結束位置 node *ch[26]; node *fail; node() { memset(ch,0,sizeof(ch)); fail=NULL; End=0; num=0; } void build(char *c,int i)//構建字典樹 { if(*c==‘\0‘) { End=1; if(!num) num=i; same[num].push_back(i);//如果發現這裏已經有單詞結束了,那麽一定是重復的,直接向原來的後面加編號就好了 return; } if(!ch[*c-‘a‘]) ch[*c-‘a‘]=new node(); ch[*c-‘a‘]->build(c+1,i); } }*root=new node(); char t[200][200]; node *q[1000011];//用隊列完成BFS int l=0,r=0; void Fail()//構建fail指針 { root->fail=root;//沒有這句話貌似也可以,為了保險起見,防止越界 for(int i=0;i<26;++i)//根節點的兒子失配指針都指向自己 if(!root->ch[i])//沒有這個兒子就指向失配指針的這個兒子,而失配指針是自己,為了不紊亂和方便,這個兒子就指向自己 root->ch[i]=root; else { root->ch[i]->fail=root;//設置失配指針 q[++r]=root->ch[i]; } while(l<r) { node *p=q[++l]; for(int i=0;i<26;++i) if(p->ch[i]) { p->ch[i]->fail=p->fail->ch[i];//有這個兒子就設置失配指針到自己的失配指針,自己的失配指針指向的地方一定已經完成工作了 q[++r]=p->ch[i]; } else p->ch[i]=p->fail->ch[i]; } return; } char s[1000010]; int cnt[155]; void match() { int ans=0; scanf("%s",s); node *now=root; for(int i=0;s[i]!=‘\0‘;++i)//開始匹配 { now=now->ch[s[i]-‘a‘]; cnt[now->num]+=now->End; node *p=now; while(p!=root)//暴力跳fail { p=p->fail; cnt[p->num]+=p->End; } } } int main() { int n; scanf("%d",&n); while(n) { root=new node(); memset(cnt,0,sizeof(cnt)); for(int i=1;i<=n;++i) { scanf("%s",t[i]); root->build(t[i],i); } Fail(); match(); int mx=0; for(int i=1;i<=n;++i) { for(vector<int>::iterator it=same[i].begin();it!=same[i].end();++it)//處理相同模式串 cnt[*it]=cnt[i]; if(mx<cnt[i]) { cnt[0]=1; mx=cnt[i]; } else if(mx==cnt[i]) ++cnt[0]; } printf("%d\n",mx); for(int i=1;i<=n;++i) if(cnt[i]==mx) printf("%s\n",t[i]); scanf("%d",&n); } return 0; }
【AC自動機】【字符串】【字典樹】AC自動機 學習筆記