在沒學AC自動機之前以為這是一個很高深很難的算法,但其實AC自動機並不難,理解之后就變得非常簡單了。
先來介紹一下AC自動機:AC自動機全稱Aho-Corasick automaton(不是Accept自動機qwq),是著名的多模匹配算法,在多模匹配問題上相比於kmp效率更快。舉個例子:詢問多個單詞在一篇文章中是否出現過,kmp要把每個單詞都和文章匹配一次,但AC自動機只要匹配一次就能知道哪些單詞出現過。
想學AC自動機的基礎是先學會trie樹(就是字典樹)和kmp(個人覺得這個沒啥必要,了解一下就好)。AC自動機上一個最重要的東西就是失配指針(fail),有了這個東西才能進行匹配。trie樹上每個點都有且只有一個失配指針,失配指針指向的是以當前點所表示的字符為最后一個字符的字符串(也就是根到當前點組成的字符串)的最長后綴的最后一個節點。舉個例子:有兩個串,分別為abc和bc,每個字符所在點的標號依次為1(a)2(b)3(c),4(b)5(c)。3的失配指針指向的點就是5,2指向的點就是4,如果根到這個點組成的字符串沒有后綴(例如1),那它的失配指針就指向根節點(根節點失配指針是自己)。
那么AC自動機是怎么匹配的呢?我們用一個圖來解釋(用圖比較形象qwq)。
這里有四個串:abcd,abd,bce,cd。圖中紅色箭頭就表示這個點的失配指針指向的點現在給出一個原文本abcecd,詢問這四個單詞有哪個在原文本中出現過。從根節點開始走發現子節點中有'a'字符(1節點),然后走到1節點,再往下走,一直走到3節點,發現3的子節點沒有'e',那就跳到3的失配指針7節點再往下發現有'e'了再走下去。到了8節點,下面沒有節點了,就跳到它的失配指針(根節點)然后繼續往下走,直到匹配結束了為止(如果匹配到一個字符,匹配不下去了且當前在根節點,就跳過這個字符匹配下一個)。原文本串在AC自動機上經過了兩個終止節點(8和10),因此這兩個串在原文本中出現過。
下面是代碼時間
建trie樹
void build(char *s) { int l=strlen(s); int now=0; for(int i=0;i<l;i++) { int x=(s[i]-'a'); if(a[now].vis[x]==0) { a[now].vis[x]=++cnt; } now=a[now].vis[x]; } a[now].end++; }
找失配指針
void bfs() { queue<int>q; for(int i=0;i<26;i++) { if(a[0].vis[i]!=0) { a[a[0].vis[i]].fail=0; q.push(a[0].vis[i]); } } while(!q.empty()) { int now=q.front(); q.pop(); for(int i=0;i<26;i++) { if(a[now].vis[i]!=0) { a[a[now].vis[i]].fail=a[a[now].fail].vis[i]; q.push(a[now].vis[i]); } else { a[now].vis[i]=a[a[now].fail].vis[i]; } } } }
AC自動機匹配
int Aho_Corasick_automaton(char *s) { int l=strlen(s); int now=0; int ans=0; for(int i=0;i<l;i++) { int x=(s[i]-'a'); now=a[now].vis[x]; for(int t=now;t&&a[t].end!=-1;t=a[t].fail) { ans+=a[t].end; a[t].end=-1; } } return ans; }
變量名解釋:fail,失配指針;end,終止節點;vis,每個點的子節點。
從AC自動機匹配的函數中可以發現並沒有失配指針什么事情qwq,其實在找失配指針時就已經把一個節點失配指針對應的子節點連在了這個點的子樹上(嚴格來講這樣做就已經把AC自動機轉化成trie圖了,真正的AC自動機是跳fail指針的,不過這樣寫時間復雜度會更優,因此也可以當做AC自動機來寫),還是以上面那個圖為例,因為3節點沒有'e'這個節點,所以就把7節點的子節點中的'e'(8節點)接在3節點下方,這樣在AC自動機匹配時直接像遍歷圖一樣往下遍歷就行了。
最后再說一下和AC自動機有關的一個東西——fail樹。AC自動機上每個節點都有一個失配指針,那么將每個點的失配指針連向這個點,就形成了一棵樹,一般稱為fail樹。在fail樹上一個節點的所有祖先節點在AC自動機上所代表的串都是這個節點在AC自動機上所代表的串的后綴,因為如果一個串X的末節點的失配指針直接或間接地指向Y串的末節點,那么Y串的末節點在fail樹上一定是X串末節點的祖先。有許多AC自動機的題都能利用fail樹的性質來解題。
推薦練習題(難度遞增):
(其中BZOJ1212可以用AC自動機,但我用了trie樹)