AC算法 及python實現


零 導言

  軟件安全課上,老師講了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。我們按照如下步驟建立失效函數:

  1. f(0)=0;
  2. d=1;
  3. 循環直到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[&current_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代碼 

  下面的AC算法並未使用雙數組的優化方法,而是使用了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/

  

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM