概述
AC自動機全稱Aho-Corasick automaton,該算法在1975年產生於貝爾實驗室,是著名的多模匹配算法。
考慮這樣一個場景,給出L個模式字符串(加總長度為N),以及長度為M大文本,要求從大文本中提取每個模式字符串出現的位置。如果使用KMP算法,時間復雜度將達到O(LM+N),而使用AC自動機可以在O(N+M)時間復雜度內解決這一問題,當L很大時,AC自動機的優勢非常明顯。
建立AC自動機
AC自動機實際上是前綴樹,但是會引入一個與KMP類似的失敗轉移的概念。我們先為所有模式建立對應的前綴樹,之后為每個前綴樹結點添加一個指針fail,指向另外一個前綴樹中的結點。每個前綴樹中的結點實際上都代表了某個模式的一段前綴,我們之后將結點與其對應的前綴等同起來。令結點x的fail指針指向y(y不為x),其中y是x的后綴,且y是所有符合這類條件的結點中深度最大的(前綴長度最大的),我們稱y是x的后綴結點,稱x是y的偽父,顯然偽父的偽父依舊還是偽父。可以很容易證明以x為起點沿着fail指針不斷移動,可以遍歷所有x的有效后綴,且訪問到的結點深度遞減。如果無法為結點的fail指針無法找到有效的結點,那么將fail指針指向前綴樹的根結點root。
AC自動機的難度在於要如何為每個結點建立fail指針。由於fail指針指向的結點深度必然小於fail指針的持有者,因此可以用DP的思路,我們先為深度較小的結點建立fail指針,再為深度較大的結點建立fail指針。這個過程可以通過廣度優先搜索算法實現。要建立x的fail指針,考慮到x.fail.father必然是x.father的某個有效后綴,因此我們可以通過以x.father為起點,沿着fail指針移動以尋找x.fail.father,並從而找到x.fail。這個過程十分類似於KMP中建立跳轉表的過程,這里對其具體操作不再贅述。
使用AC自動機
如何使用AC自動機呢?我們維護一個軌跡結點trace,對於每個輸入字符c,我們判斷trace是否有c號孩子,如果有就將trace設置為其c號孩子,否則我們將trace設置trace.fail,並繼續詢問,直到trace成為root或者找到了c號孩子。重復上面過程直到讀完文本。
若最后trace成功設置為其c號孩子,則我們稱訪問了c號孩子。可以證明若輸入文本T中T[a...b]與某個模式p相匹配,那么當我們讀入T[b]時,p和p的所有偽父中有且只有一個結點被訪問。*對於任意c<a,a=<d<b,若trace匹配T[c...d],那么當我們讀入T[d+1]時,若成功,trace將步進,若失敗,則依舊能保證trace轉移后c<=a,因為此時p的某個祖先結點已經做好了接盤的准備,故c始終會小於等於a,當c=a時,此時trace為p的祖先,因此直到讀入T[b]時,trace必定匹配T[c...b],此時c<=a,因此trace是p或p的偽父。*通過這段證明我們基本可以了解到如何在AC自動機讀取完文本后獲取我們想要的結果,如果需要每個模式出現次數,可以得知每個模式的出現次數為其被訪問次數加上其所有偽父被訪問次數,而如果需要每個模式的匹配位置,思路也是類似,為每個模式維護被訪問時讀取字符的下標就可以了,整合上所有偽父的匹配位置即可得出。
時間復雜度
時間復雜度分為建立AC自動機的時間復雜度和匹配的時間復雜度。
設所有模式的長度和為n,文本長度為m。建立前綴樹的時間復雜度為O(n),而建立fail指針的時間復雜度分析類似於KMP算法中建立跳轉表的時間復雜度。我們可以定義每個結點x的fail指針指向的y結點的深度為x的“子深”,記作x.cd。很容易發現x.cd<=x.father.cd+1,而我們每次從x.father出發沿着fail指針移動,x的子深也在不斷遞減但不會低於0,在為某個模式上的結點建立fail時,每次后移最多提供1個子深,因此在創建模式pi時我們最多沿着fail指針移動了|pi|次,故創建所有模式總共沿着fail指針最多移動O(n)次,到此說明了建立fail指針的時間復雜度為O(n)。
對模式匹配,每當我們讀入一個字符c時,trace或者向下移動(即有c號孩子)並結束或者沿着fail移動到某個自己的后綴上去。顯然向下移動最多發生O(m)次,而沿着fail移動,就如同我所說的每次都必定會降低子深,而每次向下移動可以提供最多1子深,因此可以保證沿着fail移動的次數最多為O(m)次。故總的時間復雜度為O(m)。
時間復雜度的總和為O(n+m),空間復雜度為O(Cn),其中C為使用的字符集的大小(用於建立前綴樹)。