DFA 確定性有限狀態自動機
DFA確定性有限狀態自動機是一種圖結構的數據結構,可以由(Q, q0, A, Sigma, Delta)來描述,其中Q為狀態集,q0為初始狀態,A為終態集合,Sigma為字母表,Delta為轉移函數。它表示從唯一一個起始狀態q0開始,經過有限步的Delta轉移,轉移是根據字母表Sigma中的元素來進行,最終到達終態集合A中的某個狀態的狀態移動。
如圖所示是一個終態集合為{"nano"}的DFA。
DFA只能有一個起點而可以有多個終點。每個節點都有字符集大小數條有向邊,並且任一節點,都不會存在相同字符的有向邊指向不同的節點。
Trie樹
Trie樹為單詞前綴樹,m個模式串中的前綴所組成的集合A與根節點到每一個樹中的節點的路徑上的字符組成的字符串S所組成的集合B,是滿射關系。具體參見:Trie樹
例如可以利用Trie樹求字符串S的所有不同子串:
假設當前字符串為S,用S的所有后綴作為len(S)個模式串,插入到一棵Trie樹中,Trie樹中的每個節點對應的字符串就是字符串S中的一個子串,不同子串一定對應不同的節點。
Trie圖
1. Trie圖結構
Trie圖為以Trie樹為基礎構造出來的一種DFA。對於插入的每個模式串,其插入過程使用的最后一個節點都作為DFA的一個“終止”節點。
如果要求一個母串包含哪些模式串(即該母串的某個子串恰好等於預先給定的某個模式串),以母串作為DFA的輸入 ,在DFA上行走,走到“終止”節點就意味着匹配了相應的模式串。
2. 失配處理
在行走的過程中,如果出現母串中的下一個字符在Trie圖中當前位置處沒有一個子節點與之對應,或者Trie圖中的當前位置正好匹配了一個模式串,那么需要調整母串重新匹配的位置。一般,可以調整母串上開始匹配的位置,使之加1,再嘗試從Trie圖的根節點位置開始匹配。這樣顯然效率很低。可以參考KMP算法的最長相同前后綴的方法,來避免回溯。
3. 前綴指針
為了避免回溯,參考KMP的next數組,在Trie圖中定義“前綴指針”:
從根節點到節點P可以得到一個字符串S,節點P的前綴指針定義為 指向樹中出現過的S的最長后綴(不能等於S)
4. 高效的構造前綴指針
根據深度一一求出每一個節點的前綴指針。對於當前節點,設它的父節點與它的邊上的字符為ch,如果它的父節點的前綴指針所指向的節點的兒子節點中,有通過ch字符指向的兒子,那么當前節點的前綴指針指向該兒子節點,否則通過當前節點父節點的前綴指針指向節點的前綴指針,繼續向上查找,直到到達根為止。
5. 危險節點
(1)“終止”節點是危險節點
(2)如果一個節點的前綴指針指向危險節點,則該節點為危險節點
6. 在建立好的Trie圖上遍歷
從root出發,按照當前串的下一個字符ch進行在樹上的移動。若當前點P不存在通過ch連接的兒子,那么將P的前綴指針所指向的節點Q作為當前節點;
如果還無法找到通過ch連接的兒子節點,再考慮Q的前綴指針指向的節點(將之作為當前節點)....;
直到找到通過ch連接的兒子,再繼續遍歷;
如果遍歷的過程中經過了某個非終止節點的危險節點,則可以斷定S包含某個模式串,要找出是哪個模式串,沿着危險節點的前綴指針走,碰到終止節點即可。
7. Trie圖的時間復雜度
在Trie圖上遍歷母串S的時間復雜度為len(S)。
(1)母串每過掉一個字符,不論該字符是匹配上還是沒匹配上,在trie圖上最多向下走一層
(2)一個節點的前綴指針總是指向更高層次的節點,所以每次沿着前綴指針走一步,節點的層次就會向上一層
(3)母串S最終被過掉了len(S)個字符,所以最多向下走了len(S)次。
(4)最多向下走了len(S)次,那么就不可能向上走超過len(S)次,因此沿着前綴指針走的次數,做多不超過len(S)
前綴指針思想
Trie通過前綴指針來避免母串的回溯,其思想和KMP算法非常相似。
KMP算法是通過確定子串中失配點之前的子串的最長相同前后綴,失配時,母串當前點不回溯,而是直接和最長相同前后綴的前綴處繼續進行匹配。
(kmp 避免母串指針回溯)
和KMP類似,Trie圖中的每個節點都對應一個模板串(節點為終止節點)或者模板串的子串(節點不是終止節點),記為S。S可以確定len(S)-1個后綴(從S中的第2到第len(S)-1個位置到S的末尾確定),其中有些后綴串Si可能正好對應該Trie圖中從root節點出發的到某個節點Pi確定的串。
如上圖所示,綠色方塊區域為從母串上一個開始匹配點到失配點之前的匹配區域,紅色為失配點,該綠色匹配區域中有兩個后綴子串sub1[S1,A]區域和sub2[S2,A]區域,分別對應Trie圖中從root出發到P1,P2點確定的串。且母串中[S1,E1]和[S2,E2]分別對應一個模式串。
母串不回溯,Trie圖上當前點的移動,可以匹配母串中存在的所有的模式串
以上圖為例此時,需要確定母串指針之后的移動可以找到[S1,E1]和[S2,E2]兩個模式串,策略是先匹配起點靠前的那個串,即[S1,E1]。
case 1 E1 > E2
母串指針不回溯,Trie圖的當前點轉移到P1(從root到P1對應[S1,A]),然后嘗試匹配。匹配成功,到達E1點,此時將Trie圖中點從E1移動到E1的前綴指針,由於[S2,E1]為[S1,E1]的最長后綴,即移動到點P,使得root到P為[S2,E1]。因為Trie圖中[S2,E2]對應從root出發到某個點Q的串,那么root到Q的路徑必然經過點P,此時從點P繼續匹配,必然能夠到達Q;
這樣,就得到了[S1,E1]和[S2,E2]兩個模式串。
case 2 E1 < E2
母串指針不回溯,Trie圖的當前點轉移到P1(從root到P1對應[S1,A]),然后嘗試匹配。由於[S1,E1]對應一個模式串,即對應Trie圖中的某個終止節點,從P1點開始會一直匹配到達P(從root到P對應[S1,E1])。在匹配的過程中,會碰到某個點危險節點M,M指向節點Q(從root到Q對應[S2,E2])(這是在設置Trie圖中各個節點的前綴指針的時候確定的),根據Trie圖的遍歷規則,會得到[S2,E2]的模式串。這樣,就得到了[S1,E1]和[S2,E2]兩個模式串。
Trie圖實現(c++)
#include<iostream> #include<vector> #include<queue> #include<string> using namespace std; #define LETTERS 26 int gNodeCount = 2; struct Node{ Node* childs[LETTERS]; //子節點 Node* prev; //前綴指針 bool danger_node; //是否危險節點 Node(){ Init(); } void Init(){ memset(childs, 0, sizeof(childs)); danger_node = false; prev = NULL; } }; Node gNodes[2000]; void Insert(Node* root, char* str){ char* p = str; Node* node = root; while (*p != '\0'){ int index = *p - 'A'; if (node->childs[index] == 0){ node->childs[index] = gNodes + gNodeCount ++; } node = node->childs[index]; p++; } node->danger_node = true; } //在Trie樹上添加前綴指針 void BuildDfa(){ Node* root = gNodes + 1; for (int i = 0; i < LETTERS; i++){ //為虛擬節點 gNodes[0].childs[i] = root; } root->prev = gNodes; gNodes[0].prev = NULL; deque<Node*> Q; Q.push_back(root); while (!Q.empty()){ Node* node = Q.front(); Node* prev = node->prev, *p; Q.pop_front(); for (int i = 0; i < LETTERS; i++){ if (node->childs[i]){ p = prev; while (p && !p->childs[i]){ p = p->prev; } node->childs[i]->prev = p->childs[i]; //這個地方注意,不能寫成 p->childs[i]->danger_node = node->childs[i]->danger_node if (p->childs[i]->danger_node) node->childs[i]->danger_node = true; Q.push_back(node->childs[i]); } } } } bool SearchDfa(char* str){ char*p = str; Node* node = gNodes + 1; while (*p != '\0'){ int index = *p - 'A'; if (node->danger_node) return true; while (node&& !node->childs[index]){ node = node->prev; }
p++; } return false; }