1. AC自動機的功能:
用於多模匹配,所謂多模匹配,就是給定一個帶匹配的字符串string,給定一個字典dictionary,dictionary中有多個字符串{ str1,str2, str3 … } 多模匹配就是要得到string字符串中出現了dictionary的哪些字符,且這些字符出現在了string中的哪個位置。
2. AC自動機的原理:
AC自動機的難點在於構建一個DFA(確定狀態的有限狀態自動機)。構建這個自動機分爲兩步:
1. 根據dictionary構建一棵前綴樹trieTree。
2. 在對這棵trieTree進行BFS廣度優先遍歷的同時,爲這棵樹的節點增加邊與fail指針。
2.1介紹什麼是trieTree(前綴樹)
前綴樹是一種存儲單詞的數據結構,從樹根遍歷到每一片樹葉(或者某些中間結點)都是一個單詞,兩個單詞如果有相同的前綴,那麼在這棵樹上,從根節點到這個相同的前綴結束之前,這兩個單詞所對應的路徑是重疊的。
上圖中,從根節點到每個紅色的節點經過的路徑上的字母組成了一個字典中的單詞。
以下是trieTree節點的結構:
typedef struct trieNode {
trieNode*next[KIND]; //初始化都爲NULL ,該節點的孩子
trieNode *fail;
char value[50];//存放根節點到當前節點的路徑上的值
int finalSig; //表明是否是某段字符串的最後一個值
} trieNode; //trie樹的節點
2.2 如何在廣度優先搜索的基礎上爲trieTree加邊,同時增加fail指針,形成一個DFA
1. 對trieTree根節點的直接孩子做特殊處理:
For( int i=0; i<KIND;++i ){ //KIND是字母的種類,一般是26
if( root->next[i] == NULL ){ //因爲字典裏的單詞的開頭字母可能沒有覆蓋26個字母,所以是NULL
root->next[i] = root;
} else{
Root->next[i]->fail= root; //把trieTree根節點的直接孩子的fail指針指向root節點
}
}
2. 對trieTree進行廣度優先遍歷(層序遍歷)
設每次處理的節點爲now。
If( now->next[i] ==NULL ){
now->next[i] = now->fail->next[i];
} else{
now->next[i]->fail = now->fail->next[i];
}
2.3 如何在這個DFA上進行多模匹配
匹配的方法是,有三個指針,str_ptr指向string,now_ptr隨着str_ptr在DFA上移動,每當now_ptr移動到一個節點,就開啓一個循環:
temp_ptr = now_ptr
while( temp_ptr != root){
//如果temp_ptr指向的當前節點是一個單詞的終節點,輸出這個單詞
temp_ptr = temp_ptr -> fail //temp_ptr沿着fail指針跳躍
}
2.4 這樣做爲什麼就可以進行多模匹配,不會漏掉某個隱藏在string中的某個dictionary字符串
1. 爲什麼通過fail指針跳轉到的節點(記爲j),從根到該跳轉節點j之間路徑對應的單詞已經匹配成功?
首先要知道一個條件:fail指針指向的節點值要麼和自己相同,要麼指向root。在此基礎上看下面的敘述。
因爲:假設原節點爲a,跳轉後的節點爲A。兩個節點之間通過fail指針相連。所以a=A,或者A是樹根節點root。
原節點a的父節點爲Pa,跳轉節點A的父節點是PA,根據fail指針的生成過程,Pa與PA之間也是由fail指針相連的。 所以Pa=PA,或者PA是樹根節點root。
以此類推,我們就可以知道命題是成立的(更規範的證明可以用數學歸納法)。
相對於原來的trieTree,構建DFA新加入了很多邊來把原來節點中next[]數組裏的NULL指針填補上,這些新加入的邊我稱做jump。Jump邊的加入過程通過觀察源代碼:
If( now->next[i] ==NULL ){
now->next[i] = now->fail->next[i]; //添加jump邊(指針)
} else{
now->next[i]->fail = now->fail->next[i]; //添加fail邊(指針)
}
Jump邊感覺和fail邊很類似。其實可以根據上面的證明類比得到,通過jump邊跳轉到的節點,根到跳轉節點之間的路徑對應的單詞也是匹配成功的。
2. 定理:沿着fail邊跳轉到的節點所代表的單詞是跳轉前節點對應單詞的最長後綴
證明:
從上到下,第一個是原來的串str1 第二個是沿着fail跳轉後的串str2,假設str2不是str1的最長後綴,而是str3,即第三個串。
藍色部分的fail指針必然指向root,如果存在str3,那麼str1黃色部分的fail指針將指向str3的黃色部分,從而影響後面的fail指針的分佈,導致沿着str1末尾的fail跳轉會到達str3。
同理可以證明jump沿着邊跳轉也有類似的性質。
1. 爲什麼不會漏掉某個隱藏在string中的dictionary裏的字符串
先理解2.3節多模匹配過程。
參考2.3節多模匹配的過程,我把DFA中的原來屬於trieTree的邊叫做trie邊,生成DFA過程中用來填補每個節點next[]中的NULL的邊叫做jump邊,還有每個節點中的fail邊。(紫色是引用2.3的多模匹配過程)
匹配的方法是,有三個指針,str_ptr指向string,now_ptr隨着str_ptr在DFA上移動,每當now_ptr移動到一個節點,就開啓一個循環:
temp_ptr = now_ptr
while( temp_ptr !=root ){
//如果temp_ptr指向的當前節點是一個單詞的終節點,輸出這個單詞
temp_ptr = temp_ptr -> fail //temp_ptr沿着fail指針跳躍
}
now_ptr指針隨着str_ptr指針一同運動,now_ptr指針只走DFA中的trie邊和jump邊,並不走fail邊。
str_ptr指向了在string中當前匹配的末位置,爲了更好的說明匹配過程,再增加一個虛擬的string中的指針begin_ptr,這個指針指向當前可能匹配成功的單詞在string中的開始位置(初始狀態下,begin_ptr就在string的開頭)。
l 先討論now_ptr運動的過程:
now_ptr在trie邊上運動的時候,string中的指針只是str_ptr在一起運動,begin_ptr並不動。當now_ptr通過jump邊跳到一個節點後,這時begin_ptr就相當於向右移動了一段距離,變換了當前可能匹配成功的單詞。詳見下圖:
黃色部分是原來認爲可能匹配成功的單詞(的前綴),藍色部分是jump邊跳躍的部分在string上的體現,紅色部分是當前認爲可能匹配成功的單詞(的前綴),綠色部分是begin_ptr跳躍的一段距離。由於沿着jump邊跳轉,切換的串是跳轉前節點對應單詞的最長後綴,所以不會遺漏。
l 再看看temp_ptr的運動過程
每當now_ptr到達一個節點,相當於確定了一個可能成功匹配的單詞的前綴PREFIX,temp_ptr就從now_ptr開始,順着fail邊不斷向後找,直到找到root,這一在DFA上的過程對應到string上就是不斷地更換PREFIX的後綴,試圖找到匹配的單詞。由於沿着fail邊跳轉,切換的串是跳轉前節點對應單詞的最長後綴,所以不會遺漏。