今天說說多模式匹配AC算法(Aho and Corasick),感謝追風俠幫忙整理資料,while(1) {Juliet.say("3Q");}。前面學習了BM、Wu-Manber算法,WM由BM派生,不過AC與它們無染,是另外一種匹配思路。
1. 初識AC算法 |
Step1: 將由patterns組成的集合(要同時匹配多個patterns嘛)構成一個有限狀態自動機。
Step2: 將要匹配的text作爲自動機輸入,輸出含有哪些patterns及patterns在全文中位置。
自動機的執行動作由三個部分組成:
(1) 一個goto function
(2) 一個failure function
(3) 一個output function
我們先通過一個具體實例來了解一下這三個部分,及該自動機的運作方式。先有個大概印象,後面會具體講解。patterns集合{he, she, his ,hers},我們要在”ushers”中查找並匹配。
(1) goto function
(q)-----------------a--------------->(r)
狀態q到r,構成邊a,
g(q,a) = r
(2) failure function
i 1 2 3 4 5 6 7 8 9
f(i) 0 0 0 1 2 0 3 0 3 (發現沒?貌似i和f(i)有相同前綴哦^_^)
(3) output function
i output(i)
2 {he}
5 {she,he}
7 {his}
9 {hers}
///////////////////////////////////////////////////////////////////////
首先我們從狀態0開始,接收匹配字符串的第一個字符u,在goto(簡稱goto function)中可以看到回到狀態0,接着第二個字符s,發現轉到狀態3,在output中查找一下output(3)爲空字符串,說明沒有匹配到patterns。繼續匹配h,轉到狀態4,查找output發現仍然沒有匹配,繼續字符e,狀態轉到了5,查找output,發現output(5)匹配了兩個字符串she和he,並輸出在整個字符串中的位置。然後接着匹配r,但發現g(5,r)=fail,這時候我們需要查找failure,發現f(5)=2,所以就轉到狀態2,並接着匹配r,狀態轉移到了8,接着匹配s,狀態轉移到了9,查看output,並輸出output(9):hers,記錄下匹配位置。至此字符串ushers匹配完畢。
ushers:
g(0,u) = 0
g(0,s) = 3 ---> output(3)=$(空)
g(3,h) = 4 ---> output(4)=$
g(4,e) = 5 ---> output(5):she he RECOARD
g(5,r) = fail ---> f(5)=2
g(2,r) = 8 ---> output(8)=$
g(8,s) = 9 ---> output(9):hers RECOARD
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
具體的匹配算法如下:
算法1. Pattern matching machine 輸入:text和M。text是x=a1a2...an,M是模式匹配自動機(包含了goto函數g(),failure函數f(),output函數output()) 輸出:text中出現的pat以及其位置。
state←0 for i←1 until n do //吞入text的ai while g(state, ai)=fail state←f(state) //直到能走下去,呵呵,至少0那個狀態怎麼着都能走下去 state←g(state,ai) //得到下一個狀態 if output(state)≠empty //能輸出就輸出 print i; print output(state) |
AC算法的時間複雜度是O(n),與patterns的個數及長度都沒有關係。因爲Text中的每個字符都必須輸入自動機,所以最好最壞情況下都是O(n),加上預處理時間,那就是O(M+n),M是patterns長度總和。
2. 構造三表 |
OK,下面我們看看如何通過patterns集合構造上面的3個function
這三個function的構造分兩個階段:
(1) 我們決定狀態和構造goto function
(2) 我們計算得出failure function
而output function的構造貫穿於這兩個階段中。
2.1 goto 與 ouput填表 |
我們仍然拿實例來一步步構造:patterns集合{he,she,his,hers}
首先我們構造patterns he
然後接着構造she
再構造his,由於在構造his的時候狀態0接收h已經能到狀態1,所以就不用重新建一個狀態了,有點像建trie樹的過程,共用一段相同的前綴部分
最後構造hers
具體構造goto function算法如下:
算法2. Construction of the goto function 輸入:patterns集合K={y1,y2,...,yk} 輸出:goto function g 和output function output的中間結果
/* We assume output(s) is empty when state s is first created, and g(s,a)=fail if a is undefined or if g(s,a) has not yet been defined. The procedure enter(y) inserts into the goto graph a path that spells out y. */
newstate←0 fori ← 1until k //對每個模式走一下enter(yi),要插到自動機裏來嘛 enter(yi) for all a such that g(0,a)=fail g(0,a)←0
enter(a1a2...am) { state←0;j←;1 while g(state,aj)≠fail //能走下去,就儘量延用以前的老路子,走不下去,就走下面的for()拓展新路子 state←g(state,aj) j←j+1
for p←j until m //拓展新路子 newstate←newstate+1 g(state,ap)←newstate state←newstate
output(state)←{a1a2...am} //此處state爲每次構造完一個pat時遇到的那個狀態 } |
2.2 Failure 與 output填表 |
Failure function的構造:(這個比較抽象)
大家注意狀態0不在failure function中,下面開始構造,首先對於所有depth爲1的狀態s,f(s)=0,然後歸納爲所有depth爲d的狀態的failure值都由depth-1的狀態得到。
具體講,在計算depth爲d的所有狀態時候,我們會考慮到每一個depth爲d-1的狀態r
1. 如果對於所有的字符a,g(r,a)=fail,那麼什麼也不做,我認爲這時候r已經是trie樹的葉子結點了。
2. 否則的話,如果有g(r,a)=s,那麼執行下面三步
(a) 設置state=f(r) //用state記錄跟r共前綴的東東
(b) 執行state=f(state)零次或若干次,直到使得g(state,a)!=fail(這個狀態一定會有的因爲g(0,a)!=fail)//必須找條活路,能走下去的
(c) 設置f(s)=g(state,a),即相當於找到f(s)也是由一個狀態匹配a字符轉到的狀態。
實例分析:
首先我們將depth爲1的狀態f(1)=f(3)=0,然後考慮depth爲2的結點2,6,4
計算f(2)時候,我們設置state=f(1)=0,因爲g(0,e)=0,所以f(2)=0;
計算f(6)時候,我們設置state=f(1)=0,因爲g(0,i)=0,所以f(6)=0;
計算f(4)時候,我們設置state=f(3)=0,因爲g(0,h)=1,所以f(4)=1;
然後考慮depth爲3的結點8,7,5
計算f(8)時候,我們設置state=f(2)=0,因爲g(0,r)=0,所以f(8)=0;
計算f(7)時候,我們設置state=f(6)=0,因爲g(0,s)=3,所以f(7)=3;
計算f(5)時候,我們設置state=f(4)=1,因爲g(1,e)=2,所以f(5)=2;
最後考慮depth爲4的結點9
計算f(9)時候,我們設置state=f(8)=0,因爲g(0,s)=3,所以f(9)=3;
具體算法:
算法3. Construction of the failure function 輸入:goto function g and output function output from 算法2 輸出:failure function f and output function output
queue←empty for each a such that g(0,a)=s≠0//其實這是廣搜BFS的過程 queue←queue∪{s} f(s)←0
while queue≠empty pop(); for each a such that g(r,a)=s≠fail //r是隊列頭狀態,如果r遇到a能走下去 queue←queue∪{s} //那就走 state←f(r) //與r同前綴的state while g(state,a)=fail //其實一定能找着不fail的,因爲至少g(0,a)不會fail state←f(state)
f(s)←g(state,a) //OK,這一步相當於找到了s的同前綴狀態,即f(s)
output(s)←output(s)∪output(f(s)) //建議走一下例子中g(4,e)=5的例子,然後ouput(5)∪output(2)={she,he} |
2.3 output |
Output function的構造參見算法2,3
3. 算法優化 |
改進1:觀察一下算法3中的failure function還不夠優化
我們可以看到g(4,e)=5,如果現在狀態到了4並且當前的字符爲t!=e,因爲g(4,t)=fail,
所以就根據f(4)=1,跳轉到狀態1,而之前已經知道t!=e,所以就沒必要跳到1,而直接跳到狀態f(1)=0。
爲了避免不必要的狀態遷移,和KMP算法有異曲同工之處。重新定義了一個failure function:f1
f1(1)=0, 對於i>1,如果能使狀態f(i)轉移的所有字符也能使i狀態轉移,那麼f1(i)=f1(f(i)), 否則f1(i)=f(i)。 |
改進2:由於goto function中並不是每個狀態對應任何一個字符都有狀態遷移的,當遷移爲fail的時候,我們就要查failure function,然後換個狀態遷移。現在我們根據goto function和failure function來構造一個確定的有限自動機next move function,該自動機的每個狀態遇到每個字符都可以進行狀態遷移,這樣就省略了failure function。
構造next move function的算法如下:
算法4:Construction of a deterministic finite automaton 輸入:goto functioni g and failure function f 輸出:next move function delta
queue←empty for each symbol a delta(0,a)←g(0,a) if g(0,a)≠0 queue←queue∪g(0,a)
while queue≠empty pop() for each symbol a if g(r,a)=s≠fail queue←queue∪{s} delta(r,a)←s else delta(r,a)←delta(f(r),a) |
Next function delta的計算如下:
其中’.’表示除了該狀態能識別字符的其他字符。
改進2有利有弊:好處是能減少狀態轉移的次數;壞處是由於狀態與狀態之間的遷移多了,導致存儲的空間比較大。