[Alg] 文字匹配-多模匹配-AC自動機
1. 簡介
AC自動機是一種多模匹配的文字匹配演算法。
如果採用naive的方法,即依次比較文字串s中是否包含模式串p1, p2,...非常耗時。考慮到這些模式串中可能具有相同子串,可以利用已經比較過的那些模式串的一些資訊,來優化效率。容易想到的一種方法是為這些模式串構建一個trie樹,可以較好的利用模式串的公共字首資訊。
但是如果只是採用普通的trie樹,仍有 如果一個模式串P1不匹配,就要重新回到根節點再找下一個模式串P2,也就是對於下一個模式串P2,要從P2的起始元素開始,依次與文字串S比較。這同樣不夠高效。P2如果可以利用和P1的一些共性資訊,使得可以從P2的儘可能靠後的元素開始,與文字串S比較,那麼演算法的時間複雜度可能會有效降低。
AC自動機採用了KMP演算法找next的思路,為trie樹中每個節點找fail節點。
KMP中next與AC中fail的區別:
KMP演算法 中 next[j] = k 表示字首 [0 ~ (k - 1)] 與 字尾 [(j - k) ~ (j - 1)] 這k個元素是對應相等的,但是P[j] 和 P[k] 是不一定相等要在下一次進行比較的;
AC自動機中則是 cur = cur.fail,也即從 [根節點 ~ cur.fail (包含cur.fail節點)] 這個字首,與 [cur節點所在路徑上某節點 ~ cur節點(包含cur節點)] 這個字尾,對應相等。
2. 演算法原理
(轉自參考連結1)
2.1 初識AC自動機
AC自動機的基礎是Trie樹。和Trie樹不同的是,樹中的每個結點除了有指向孩子的指標(或者說引用),還有一個fail指標,它表示輸入的字元與當前結點的所有孩子結點都不匹配時(注意,不是和該結點本身不匹配),自動機的狀態應轉移到的狀態(或者說應該轉移到的結點)。fail指標的功能可以類比於KMP演算法中next陣列的功能。
我們現在來看一個用目標字串集合{abd,abdk, abchijn, chnit, ijabdf, ijaij}構造出來的AC自動機
上圖是一個構建好的AC自動機,其中根結點不儲存任何字元,根結點的fail指標為null。虛線表示該結點的fail指標的指向,所有表示字串的最後一個字元的結點外部都用紅圈表示,我們稱該結點為這個字串的終結結點。每個結點實際上都有fail指標,但為了表示方便,本文約定一個原則,即所有指向根結點的 fail虛線都未畫出。
從上圖中的AC自動機,我們可以看出一個重要的性質:每個結點的fail指標表示由根結點到該結點所組成的字元序列的所有後綴 和 整個目標字串集合(也就是整個Trie樹)中的所有字首 兩者中最長公共的部分。
比如圖中,由根結點到目標字串“ijabdf”中的 ‘d’組成的字元序列“ijabd”的所有後綴在整個目標字串集{abd,abdk, abchijn, chnit, ijabdf, ijaij}的所有字首中最長公共的部分就是abd,而圖中d結點(字串“ijabdf”中的這個d)的fail正是指向了字元序列abd的最後一個字元。
2.2 AC自動機的執行過程:
1)表示當前結點的指標指向AC自動機的根結點,即curr = root
2)從文字串中讀取(下)一個字元
3)從當前結點的所有孩子結點中尋找與該字元匹配的結點,
若成功:判斷當前結點以及當前結點fail指向的結點是否表示一個字串的結束,若是,則將文字串中索引起點記錄在對應字串儲存結果集合中(索引起點= 當前索引-字串長度+1)。curr指向該孩子結點,繼續執行第2步
若失敗:執行第4步。
4)若fail == null(說明目標字串中沒有任何字串是輸入字串的字首,相當於重啟狀態機)curr = root, 執行步驟2,
否則,將當前結點的指標指向fail結點,執行步驟3)
2.3 例子
來一個具體的例子加深理解,初始時當前結點為root結點,我們現在假設文字串text = “abchnijabdfk”。
圖中的紫色實曲線表示了整個搜尋過程中的當前結點指標的轉移過程,結點旁的文字表示了當前結點下讀取的文字串字元。比如初始時,當前指標指向根結點時,輸入字元‘a’,則當前指標指向結點a,此時再輸入字元‘b’,自動機狀態轉移到結點b,……,以此類推。圖中AC自動機的最後狀態只是恰好回到根結點,並不一定都會回到根節點。
需要說明的是,當指標位於結點b(圖中曲線經過了兩次b,這裡指第二次的b,即目標字串“ijabdf”中的b),這時讀取文字串字元下標為9的字元(即‘d’)時,由於b的所有孩子結點(這裡恰好只有一個孩子結點)中存在能夠匹配輸入字元d的結點,那麼當前結點指標就指向了結點d,而此時該結點d的fail指標指向的結點又恰好表示了字串“abc”的終結結點(用紅圈表示),所以我們找到了目標字串“abc”一次。這個過程我們在圖中用虛線表示,但狀態沒有轉移到“abd”中的d結點。
在輸入完所有文字串字元後,我們在文字串中找到了目標字串集合中的abd一次,位於文字串中下標為7的位置;目標字串ijabdf一次,位於文字串中下標為5的位置。
3. 構造AC自動機的方法與原理
首先我們將所有的目標字串插入到Trie樹中,然後通過廣度優先遍歷為每個結點的所有孩子節點的fail指標找到正確的指向。
確定fail指標指向的問題和KMP演算法中構造next陣列的方式如出一轍。具體方法如下
1)將根結點的所有孩子結點的fail指向根結點,然後將根結點的所有孩子結點依次入列。
2)若佇列不為空:
2.1)出列,我們將出列的結點記為curr, failTo表示curr的fail指向的結點,即failTo = curr.fail
2.2) a.判斷curr.child[i] == failTo.child[i]是否成立,
成立:curr.child[i].fail = failTo.child[i],
不成立:判斷 failTo == null是否成立
成立: curr.child[i].fail == root
不成立:執行failTo = failTo.fail,繼續執行2.2)
b.curr.child[i]入列,再次執行再次執行步驟2)
若佇列為空:結束
4. 程式碼實現
1 #coding:utf-8 2 import queue 3 4 class Node(object): 5 def __init__(self): 6 self.children = {} 7 self.fail = None 8 self.isWord = False 9 self.word = "" 10 11 class ACAutomation(object): 12 """ AC Automation 13 """ 14 def __init__(self): 15 self.root = Node() 16 17 def add(self, word): 18 cur_node = self.root 19 for char in word: 20 if char not in cur_node.children: 21 cur_node.children[char] = Node() 22 cur_node = cur_node.children[char] 23 cur_node.isWord = True 24 cur_node.word = word 25 26 def link_fail(self): 27 que = queue.Queue() 28 que.put(self.root) 29 30 while que.empty() == False: 31 32 cur_node = que.get() 33 cur_fail = cur_node.fail 34 35 for child_key, child_value in cur_node.children.items(): 36 37 while True: 38 if cur_fail is None: 39 cur_node.children[child_key].fail = self.root 40 break 41 42 elif child_key in cur_fail.children: 43 cur_node.children[child_key].fail = cur_fail.children[child_key] 44 break 45 46 else: 47 cur_fail = cur_fail.fail 48 49 que.put(cur_node.children[child_key]) 50 51 52 def curWords(self, cur_node): 53 """ 該函式為查詢當前節點處所有可能的匹配的模式串的集合 54 Args: 55 cur_node 當前節點 56 Returns: 57 set 當前節點處所有可能的匹配的模式串的集合 58 59 匹配成功模式串有兩種情況: 60 1. 當前節點處 isWord = True, 則匹配的模式串即為 cur_node.word (如圖例'ijabdf') 61 2. 當前節點的fail節點處 isWord = True, 則匹配的模式串為 cur_node.fail.word 62 (如圖例 'abd',文字標紅處有解釋) 63 (當然fail節點也可能有fail.fail...需要while迴圈繼續推一下.) 64 """ 65 ret = set() 66 cur_fail = cur_node.fail 67 if cur_node.isWord: 68 ret.add(cur_node.word) 69 #ret.append(cur_node.word) 70 while cur_fail is not None and cur_fail is not self.root: 71 if cur_fail.isWord: 72 #ret.append(cur_fail.word) 73 ret.add(cur_fail.word) 74 cur_fail = cur_fail.fail 75 return ret 76 77 def search(self, s): 78 cur_node = self.root 79 # result = [] 80 result = set() 81 cur_pos = 0 82 83 while cur_pos < len(seq): 84 word = seq[cur_pos] 85 #result.extend(self.curWords(cur_node)) 86 87 while word in cur_node.children == False and cur_node != self.root: 88 # result.extend(self.curWords(cur_node)) 89 result |= self.curWords(cur_node) 90 cur_node = cur_node.fail 91 92 if word in cur_node.children: 93 #result.extend(self.curWords(cur_node)) 94 result |= self.curWords(cur_node) 95 cur_node = cur_node.children[word] 96 97 else: 98 cur_node = self.root 99 100 result |= self.curWords(cur_node) 101 102 cur_pos += 1 103 104 return list(result) 105 106 ac = ACAutomation() 107 l = ['abd', 'abdk', 'abchijn', 'chnit', 'ijabdf', 'ijaij'] 108 for e in l: 109 ac.add(e) 110 ac.link_fail() 111 seq = 'abchnijabdfk' 112 ret = ac.search(seq) 113 print(ret) 114 115 """output 116 """ 117 # ['abd', 'ijabdf']
參考連結:
1. 多模字串匹配演算法之AC自動機—原理與實現:https://www.cnblogs.com/nullzx/p/7499397.html
2. 從頭到尾徹底理解KMP:https://www.cnblogs.com/zhangtianq/p/5839909.html