零 導言
軟件安全課上,老師講了AC算法,寫個博客,記一下吧。
那么AC算法是干啥的呢?
——是為了解決多模式匹配問題。換句話說,就是在大字符串S中,看看小字符串s1, s2,...有沒有出現。
AC算法的時間復雜度是線性的,思路非常巧妙,也挺好理解的。但是有些的對於AC算法的介紹,挺難看懂的。這是因為原始的AC算法,會存在內存占用過多的問題,因為我們引入了”雙數組“的方法來減少內存占用。所以,實際運用的AC算法,都是用雙數組的方法寫的。
一 基本信息
首先要說明的是,AC全稱Aho-Corasick,由Alfred V.Aho和Margaret J.Corasick提出。
1975年,AC算法產生於貝爾實驗室,該算法應用有限自動機巧妙地將字符串比較轉化為狀態轉移。
AC算法是基於AC自動機的,那么什么是有限自動機呢,”形式語言與自動機“這門課里講了,對吧。不過就算不知道,關系也不大。
AC自動機是一種樹形自動機,包含一組狀態,每個狀態用一個數字代表。狀態機讀入文本串y中的字符,然后通過產生狀態轉移的方式來處理文本。
其實上面的都是廢話,重點是下面一句。AC自動機的行為通過三個函數來指示:轉向函數g、失效函數f和輸出函數output。
二 具體實現與實例
下面以模式集{he, she, his, hers}為例,演示AC算法的工作過程。首先,對於模式集,建一棵字典樹,就是下面這棵樹。
(一)轉向函數
首先,來張圖。下圖中的樹,結構並沒有什么變化,只是每個節點對應了一個狀態,從狀態0到狀態9。所謂狀態轉移g(i,c)=j,就是指狀態i 讀一個字符c 轉移到狀態j , 比如g(0,'h')=1,g(1,'e')=2。
這個還是比較直觀的。在建立字典樹的時候,轉向函數也就建立好了。
建立轉向函數有什么用呢?你想啊,假設待匹配的字符是”hersqwew“,那么我們首先從狀態0出發,然后讀'h',根據g(0,'h')=1,我們跳到狀態1;然后讀'e',根據g(1,'e')=2,我們跳到狀態2;按照這種步驟,一直跳到狀態9,那么是不是說明待匹配的字符串中包含模式"hers"和"he"呢?
那么,可能會有人問了,要是在狀態0讀入字符‘i’,我們該跳到哪里呢?這也就是失效函數需要負責的事情。
(二)失效函數
相對於轉向函數,失效函數的構建相對來說復雜一些。
失效函數,顧名思義,就是指轉向函數失效之后,程序所執行的函數。轉向函數失效之后,失效函數將指定下一個狀態。
比如當我們發現g(0,'r')-1的時候,根據f(0)=0,我們跳到狀態0。同理,我們發現g(2,'a')=-1的時候,根據f(2)=0,我們也跳到狀態0。那么是不是都跳到狀態0呢?顯然不是。你試想,假設待匹配的字符串為”shis“,我們首先從狀態0出發,0->3->4,然后我們我們發現g(4,'i')=-1,那么按理來說,我們應該跳到狀態6,因為我們可以看得出“shis”中包含模式“his”。也就是說,應該有f(4)=1,然后通過g(1,'i')=6,我們完成從4->6的跳轉。
例子講完了,那規律是什么呢?
對於f(4)=1,我們發現有g(3,'h')=4和g(0,'h')=1。
來個復雜點的例子,對於待匹配串“shers”,它包含"she"、"he"、"hers"三個模式。當我們讀到e的時候,已經到了狀態5,再讀r,我們發現應該跳到2,然后到3,也就是有f(5)=2。在這里我們發現,g(3,'h')=4和g(0,'h')=1, 以及g(4,'e')=5和g(1,'e')=2。
規律說完了,好好理解一下。可能你會覺得構建起來,比較麻煩,但其實實現起來比較簡單。
失效函數構建方法
我們先引入深度d的概念,這里的深度和我們樹中常說的深度是一個意思,舉個例子d(1)=1,d(6)=2。我們按照如下步驟建立失效函數:
- f(0)=0;
- d=1;
- 循環直到d取最大深度{ 根據深度為d-1的函數值,計算所有深度為d的函數值; d++}
第3步中,”根據深度為d-1的函數值,計算所有深度為d的函數值”,舉個例子,對於狀態9,存在g(8,s)=9,那么f(9)=g(f(8),s)。按照這個遞推式我們就可以從深度d-1的推到深度d 的是失效函數值了。
(三)輸出函數
通過轉向函數和失效函數,我們能夠實現AC自動機一個個地讀入字符,然后進行狀態跳轉。我們還缺一個輸出結果的函數,比如當我們跳到了狀態2,我們這時應該輸出"he",當當我們跳到了狀態5,我們應該輸出"she"和"he"(注意:不僅僅是"she")。
好了,那我們怎么建立輸出函數呢?
分為兩部分。首先,在建立轉向函數g 的時候,我們就可以建立輸出函數,比如output(2)="he", output(9)="hers"等。然后,在建立失效函數f 的時候,我們更新輸出函數。具體來說,當我們發現f(s)=s'的時候,有 output(s)=output(s)∪output(s')。例如,有f(5)=2,則output(5)=output(5)∪output(2)={"she","he"}。
(四)匹配過程
在預處理階段,我們求得轉向函數g 、失效函數f 和輸出函數output。結果如下(你們可以試着求一下,對一下答案)
轉向函數 g(0,'h')=1, g(1,'e')=2, g(2,'r')=8, g(8,'s')=9,
g(1,'i')=6, g(6,'s')=7,
g(0,‘s')=3, g(3,'h')=4, g(4,'e')=5
其他為-1
失效函數 f(4)=1, f(5)=2, f(7)=3, f(9)=3, 其他為0
輸出函數 output(2)={"he"}, output(9)={"hers"}, output(5)={"she","he"}, output(7)={"his"} 其他為{}
匹配階段相對來說,比較簡單。
搜索查詢階段 文本掃描開始時,初始狀態為0,二輸入文本y 的首字符作為當前輸入字符。然后,開始按照轉向函數進行狀態轉移。如果轉移函數失敗則查詢失效函數,自動機狀態轉為失效函數定義的狀態。每次的狀態轉移都要檢查輸出函數。
也就是說,假設待匹配字符為"hisshers",那么狀態轉移為:0->1->6-> 7->3 -> 0->3 ->4-> 5->2 ->8->9,依次輸出"his"、"she"、"he"、"he"、"hers"。
三 利用雙數組進行優化
(一)為什么需要優化
在一開始,我們講到AC算法存在內存占用過多的問題。那這是怎么回事呢?
對於轉向函數g,比如g(i,c)=j ,它有兩個輸入參數: 當前狀態i 和讀入的字符c ,和一個輸出值j 。那這些怎么存儲呢?如果是C語言(不是c++, 或者python),我們肯定就是用數組存的。這樣的話,這個數組就會非常大,如果使用ASCII碼的話,每個狀態的轉向函數至少是256a個字節(a為存儲一個狀態所占的字節數)。然而,這個數組中大量的元素都為-1(NULL),也就是有大量的內存被浪費。
於是,我們引入了雙數組的改進方法。
(二)怎么通過雙數組優化
雙數組,但實際上有三個數組:next數組、base數組和check數組。(我也不知道為什么叫雙數組*_*,可能是因為雙數組方法的靈魂是base和check數組吧)
假設我們已經計算出next、base和check,那么轉向函數的偽代碼將會是這樣的
def g_index(current_state, ch):
next_state=next[¤t_state+base[current_state]+ch] if check[next_state]==curent_state: return next_state else: return -1
下面分別解釋一下三個數組
next數組 next為轉向函數表,下標是位置偏移量,輸出是狀態值。
base數組 下標是狀態值,輸出是base值。Next表中當前狀態為s,輸入為c時,假設應跳轉為狀態t,狀態t在Next表中的位置=狀態S的位置+狀態S的Base值+輸入c的ASCII碼值。
check數組 下標是狀態值,輸出是下標狀態的父狀態的值。
其實,說這么多,倒不如一個例子來的簡單。
例子
(一)
(二)
(三)
(四)
四 python代碼
此外,感謝junboli指出了代碼中的錯誤,現已修正。
# python3 from collections import defaultdict class Node: def __init__(self,state_num,ch=None): self.state_num = state_num self.ch = ch self.children = [] class Trie(Node): """ 實現了一個簡單的字典樹 """ def __init__(self): Node.__init__(self,0) def init(self): self._state_num_max = 0 self.goto_dic = defaultdict(lambda :-1) self.fail_dic = defaultdict(int) self.output_dic = defaultdict(list) def build(self,patterns): """ 參數 patterns 如['he', 'she', 'his', 'hers'] """ for pattern in patterns: self._build_for_each_pattern(pattern) self._build_fail() def _build_for_each_pattern(self,pattern): """ 將pattern添加到當前的字典樹中 """ current = self for ch in pattern: # 判斷字符 ch 是否為節點 current 的子節點 index = self._ch_exist_in_node_children(current,ch) # 不存在 添加新節點並轉向 if index == -1: current = self._add_child_and_goto(current,ch) # 存在 直接 goto else: current = current.children[index] self.output_dic[current.state_num] = [pattern] def _ch_exist_in_node_children(self,current,ch): """ 判斷字符 ch 是否為節點 current 的子節點,如果是則返回位置,否則返回-1 """ for index in range(len(current.children)): child = current.children[index] if child.ch == ch: return index return -1 def _add_child_and_goto(self,current,ch): """ 在當前的字典樹中添加新節點並轉向 新節點的編號為 當前最大狀態編號+1 """ self._state_num_max += 1 next_node = Node(self._state_num_max,ch) current.children.append(next_node) # 修改轉向函數 self.goto_dic[(current.state_num,ch)] = self._state_num_max return next_node def _build_fail(self): node_at_level = self.children while node_at_level: node_at_next_level = [] for parent in node_at_level: node_at_next_level.extend(parent.children) for child in parent.children: v = self.fail_dic[parent.state_num] while self.goto_dic[(v,child.ch)] == -1 and v != 0: v = self.fail_dic[v] fail_value = self.goto_dic[(v,child.ch)] self.fail_dic[child.state_num] = fail_value if self.fail_dic[child.state_num] != 0: self.output_dic[child.state_num].extend(self.output_dic[fail_value]) node_at_level = node_at_next_level class AC(Trie): def __init__(self): Trie.__init__(self) def init(self,patterns): Trie.init(self) self.build(patterns) def goto(self,s,ch): if s == 0: if (s,ch) not in self.goto_dic: return 0 return self.goto_dic[(s,ch)] def fail(self,s): return self.fail_dic[s] def output(self,s): return self.output_dic[s] def search(self,text): current_state = 0 ch_index = 0 while ch_index < len(text): ch = text[ch_index] if self.goto(current_state,ch) == -1: current_state = self.fail(current_state) current_state = self.goto(current_state,ch) patterns = self.output(current_state) if patterns: print (current_state,*patterns) ch_index += 1 if __name__ == "__main__": ac = AC() ac.init(['hert', 'this', 'ishe', 'hit','it']) ac.search("ithisherti")
本文鏈接:http://www.superzhang.site/blog/AC-algorithm-and-its-python-implementation/