AC自動機

2.多字符串匹配問題和Trie(字典樹):

      對於多字符串匹配問題,我們一般會用hash(散列表)或者Trie(字典樹)儲存。

      a.hash:

      將字符串利用hash函數映射到對應的hash值,然後將字符串插入對應函數值點的儲存空間。(這裏不關於hash函數的選擇和具體實現方式。)

      b.Trie:

      這是一個樹,輸的節點有|{字符集}|個指針,如果一個單詞對應的字母的x後面有字母y,那麼他的y指針就指向一個新的節點。

                     
            上圖是一個集合{he,hers,his,she}構成的字典樹。

      c.字典樹的定義:

      字典樹的節點如下面,數據分爲兩部分;一部分是指針數組,用來指向單詞的下一個字母;另一部分是數據域,存儲單詞結尾的標記、單詞計數、或者是字符串之間映射的對應串。

  1. typedef struct node {   
  2.     type save;   
  3.     node* next[LETTE_SIZE];   
  4. }tnode;  

      d.新單詞的插入:

      從根節點開始查找,如果單詞當前字母指針不空,則沿着這個指針查找;如果爲空,則插入新的節點,沿着該節點方向查找。

  1. void insert( char* word, int l, type data )  
  2. {  
  3.     tnode* now = root;  
  4.     for ( int i = 0 ; i < l ; ++ i ) {  
  5.         if ( !now->next[word[i]-'a'] )  
  6.             now->next[word[i]-'a'] = newnode();  
  7.         now = now->next[word[i]-'a'];  
  8.     }Deal(now->save, data);//處理相應操作  
  9. }  

      新節點的插入:每次插入的新節點,初始化所有指針和數據域初始爲空。

  1. tnode* newnode()   
  2. {  
  3.     for ( int i = 0 ; i < 26 ; ++ i )  
  4.         dict[size].next[i] = NULL;  
  5.     dict[size].word = NULL;  
  6.     return &dict[size ++];  
  7. }  

e.單詞查詢:

      返回對應單詞結束位置節點的save即可。

  1. type query( char* word, int l )   
  2. {  
  3.     tnode* now = root;  
  4.     for ( int i = 0 ; i < l ; ++ i ) {  
  5.         if ( !now->next[word[i]-'a'] )   
  6.             return false;  
  7.         now = now->next[word[i]-'a'];  
  8.     }return now->save;  
  9. }  

3.AC自動機的介紹:

      在此認爲大家已經有了 KMP算法以及Trie(字典樹)的基礎(如果,上面的講述不夠詳細,還請查看相關資料)。AC自動機可以理解爲 KMP算法的多模式串形式擴展。
那麼什麼是 AC自動機呢,通俗的說就是Trie的每個節點加上了一個fail指針,fail指針指向當前匹配失敗的跳轉位置,這就類似於KMP的next數組。

4.AC自動機的構造:

       既然我們知道了 AC自動機是用來做什麼的,那麼我們就來說一說怎麼在 Trie上構造 AC自動機。

       首先,我們看一下條轉時的條件,如同 KMP算法一樣, AC自動機在匹配時如果當前字符匹配失敗,那麼利用fail指針進行跳轉。由此可知如果跳轉,跳轉到的串的前綴,必爲跳轉前的模式串的後綴。由此可知,跳轉的新位置的深度一定小於跳之前的節點。所以我們可以利用 bfs在 Trie上面進行 fail指針的求解。

      下面,是具體的構造過程(和KMP是一樣的)。首先 root節點的fail定義爲空,然後每個節點的fail都取決自己的父節點的fail指針,從父節點的fail出發,直到找到存在這個字符爲邊的節點(向回遞歸),將他的孩子賦值給尋找節點。如果找不到就指向根節點,具體參照代碼:

  1. setfail()   
  2. {  
  3.     Q[0] = root;   
  4.     root->fail = NULL;  
  5.     for ( int move = 0,save = 1 ; move < save ; ++ move ) {//利用bfs求解   
  6.         tnode* now = Q[move];  
  7.         for ( int i = 0 ; i < dictsize ; ++ i )  
  8.             if ( now->next[i] ) {  
  9.             tnode* p = now->fail;//從父節點的fail節點開始  
  10.             while ( p && !p->next[i] ) p = p->fail;//尋找本節點的fail節點  
  11.             now->next[i]->fail = p?p->next[i]:root;//不存在fail賦值爲root      
  12.             Q[save ++] = now->next[i];  
  13.         }  
  14.     }  
  15. }  

      在前面的Trie建立了fail指針(虛線){其實,這兩個圖片上的字符應該是在邊上的,偷懶了,網上找的圖片,有時間回來自己做一下}

4.多串匹配:

      既然已經構造好 AC自動機,下面就是寫出他的最常見的操作,多串匹配。其實匹配過程很簡單,利用Trie匹配字符串,如果失敗利用fail指針找到下次匹配的位置即可。具體參照代碼:

  1. query( char* line )   
  2. {  
  3.     tnode* now = root;  
  4.     for ( int i = 0 ; line[i] ; ++ i ) {  
  5.         int index = ID( line[i] );//取得字符對應的邊的編號  
  6.         while ( now && !now->next[index] ) now = now->fail;//如果不能匹配,尋找fail指向的節點  
  7.         now = now?now->next[index]:root;//失敗時返回root,否則返回節點  
  8.         for ( tnode* p = now ; p ; p = p->fail )  
  9.             判斷匹配  
  10.     }  
  11. }  

5.對於 AC自動機的改進:

      通過匹配的過程我們可以看出,fail是用來尋找下次跳轉的位置的,跳轉時的 next一定是爲空的。那麼我們爲什麼不用這些 next指針直接指向下一個跳轉節點呢,那樣的話,匹配時每次去 next指針的對象即可。這個被稱作Trie圖,具體參照代碼:

  1. setfail()   
  2. {  
  3.     Q[0] = root;  
  4.     root->fail = NULL;  
  5.     for ( int move = 0,save = 1 ; move < save ; ++ move ) {  
  6.         tnode* now = Q[move];  
  7.         for ( int i = 0 ; i < dictsize ; ++ i )  
  8.         if ( now->next[i] ) {  
  9.             tnode* p = now->fail;  
  10.             while ( p && !p->next[i] ) p = p->fail;   
  11.             now->next[i]->fail = p?p->next[i]:root;  
  12.             Q[save ++] = now->next[i];  
  13.         }else now->next[i] = now!=root?now->fail->next[i]:root;//其實只多了這一句  
  14.     }  
  15. }   

6.從自動機的角度理解:

      自動機可以理解成一個有向圖,圖中的每個節點都代表一個狀態,邊上對應的是識別的字符,那麼每次識別一個字符就會發生一個狀態轉向另一個狀態。有一個初始狀態(root),很多個結束狀態(Trie中被標記的點)。

      那麼我們的匹配過程就是從 root狀態出發,利用串的字符尋找下一個狀態,每走一步就吃掉一個字符,如果發現到達標記狀態則匹配成功。


      這是一個自動機的示例,其中箭頭指向的是起始狀態(S),雙圈的代表結束狀態(C,D,E,F)

7.時間複雜度分析:

      對於Trie的匹配來說時間複雜性爲:O(max(L(Pi))L(T))其中L串的長度函數,P是模式串,T是目標串。

      對於 AC自動機來說時間複雜性爲:O(L(T)+max(L(Pi))+m)氣質m是模式串的數量。

      對於 Trie 圖 來說時間複雜性爲:O(L(T))在此的時間複雜性都是指匹配的複雜度。

      對於構造的代價是 O(sum(L(Pi)))其中sum是求和函數。

8.題目分析:

      下面對於近期所做的 AC自動機的題目加以分類總結

      a.模式匹配:這類問題一般都是統計目標串中模式串的個數。下面是oj中的題目編號,和說明:

            hdu1686 Oulipo: 尋找模式串的出現次數,可以重複及覆蓋,直接求解
            hdu2087 剪花布條: 同上
            hdu2222 Keywords Search: 同上
            hdu2896 病毒侵襲: 同上
            hdu3065 病毒侵襲持續中: 同上,不過要注意非法字符直接返回root,否則會RE
            hdu3336 Count thestring: 同上上
            zoj3228 Searching the String: 同上,不過不允許覆蓋,記錄每個狀態的最晚結束位置即可
            zoj3430 Detect the Virus: 同上上,統計很簡單,主要是編碼有點糾結

      b.字符串統計:這類題目一般都是求解某種串個數,可先求解狀態轉移矩陣然後利用矩陣乘法或 DP求解

            poj2778 DNA Sequence: 求解不包含某些子串的串的個數,利用AC自動機構造轉移矩陣,然後利用矩陣乘法求解路徑個數
            hdu2243 考研路茫茫——單詞情結:上題的升級版,做法一樣,由於長度不定最後要利用快速冪和,有點糾結
            zoj1540 Censored!: 題目和上面的類似,不過狀態過多不宜使用矩陣乘法,所以利用 DP求解
            hdu2825 Wireless Password: 統計關鍵字不少於k的串的個數,並且每個只用一次,先利用狀態壓縮 DP統計 

      c.字符串構造(AC自動機+DP):其實本組和上一組基本相同,不過都是求最優解,所以單獨拿出來了,而且沒什麼共同點

            zoj3013 Word Segmenting: 其實這個題目本來是用字典樹寫的,學了AC自動機之後就優化了一下,求解單詞覆蓋的最小失敗次數
            poj3691 DNA repair: 求解將目標串取除某些串的最少操作,改變合法狀態時如果對應不同則+1,否則不變;非法則不轉移
            zoj3545 Rescue the Rabbit: 這就是萬惡之源了,AC自動機就是爲了他學的,構造最優串,用狀態壓縮DP記錄轉移狀態,最後求解
            hdu2296 Ring: 構造一個串使其權值最大長度最小,而且要字典序最小。有點糾結,DP長度短的優先,然後字典序
            hdu3341 Lost's revenge: 傳說中的RE神題,由於狀態計算錯誤,導致RE2次,其實就是DP,不過要先將狀態分解在拼裝
            zoj3190 Resource Archiver: 本次學習的收尾題目,傳說中的神題,要先構造AC自動機,然後利用最短路優化將問題轉化爲TSP問題

9.結束語:

      AC自動機的總結到此結束,不久後還會更新,如果那裏有錯還請指出。

      最後給出本人的 AC自動機的模板,和上面題目的更加詳細的題解和代碼,請在本空間搜索即可。

  1. /*  
  2.     AC自動機模板:針對具體問題需要對 query和 makeID操作行修改。   
  3.     進行了狀態合併(如果一個串是另一個串的字串則值域可以合併),並且轉化爲 Tire圖,  
  4.     一般情況下不再使用 fail指針,而是直接使用 next指針。  
  5.     如果構造某種要求的串,可構造出不包含關鍵字的狀態轉移矩陣,然後利用矩陣乘法或 DP求解。   
  6. */    
  7.   
  8. /* AC_DFA define */  
  9. #define nodesize 100001        //節點個數   
  10. #define dictsize 26            //字符集大小    
  11.   
  12. typedef struct node1  
  13. {  
  14.     int    flag;            //值域   
  15.     int    code;            //狀態域     
  16.     node1* fail;  
  17.     node1* next[dictsize];  
  18. }tnode;  
  19. tnode  dict[nodesize+1];  
  20. tnode* Q[nodesize+1];  
  21. int    ID[256];   
  22.   
  23. class AC_DFA  
  24. {  
  25.     private:  
  26.         int    size;  
  27.         tnode* root;  
  28.     public:  
  29.         AC_DFA() {  
  30.             makeID();  
  31.             memset( dict, 0, sizeof( dict ) );  
  32.             root=NULL; size=0; root=newnode();  
  33.         }  
  34.         void makeID() {  
  35.             for ( int i = 0 ; i < 26 ; ++ i )  
  36.                 ID['a'+i] = i;  
  37.         }  
  38.         void init() {  
  39.             memset( dict, 0, sizeof( dict ) );  
  40.             root=NULL; size=0; root=newnode();  
  41.         }  
  42.         tnode* newnode() {  
  43.             dict[size].code = size;  
  44.             dict[size].fail = root;  
  45.             return &dict[ size ++ ];  
  46.         }  
  47.         void insert( char* word, int l ) {  
  48.             tnode* now = root;  
  49.             for ( int i = 0 ; i < l ; ++ i ) {  
  50.                 if ( !now->next[ID[word[i]]] )  
  51.                     now->next[ID[word[i]]] = newnode();  
  52.                 now = now->next[ID[word[i]]];  
  53.             }now->flag = 1;//now->flag ++ 用於統計  
  54.         }  
  55.         void setfail() {  
  56.             Q[0] = root; root->fail = NULL;  
  57.             for ( int move = 0,save = 1 ; move < save ; ++ move ) {  
  58.                 tnode* now = Q[move];  
  59.                 for ( int i = 0 ; i < dictsize ; ++ i )  
  60.                     if ( now->next[i] ) {  
  61.                         tnode* p = now->fail;  
  62.                         while ( p && !p->next[i] ) p = p->fail;  
  63.                         now->next[i]->fail  = p?p->next[i]:root;  
  64.                         //now->next[i]->flag += now->next[i]->fail->flag;//狀態合併,不能計數  
  65.                         Q[save ++] = now->next[i];  
  66.                     }else now->next[i] = now==root?root:now->fail->next[i];//構建 Trie圖   
  67.             }  
  68.         }  
  69.         int query( char* line, int L ) {//統計字串出現個數,可重複及交叉    
  70.             int sum = 0;  
  71.             tnode *temp,*now = root;  
  72.             for ( int i = 0 ; i < L ; ++ i ) {  
  73.                 now = now->next[ID[line[i]]];  
  74.                 temp = now;  
  75.                 while (temp && temp->flag) {  
  76.                     sum += temp->flag;  
  77.                     temp = temp->fail;  
  78.                     //temp->flag = 0 用於統計時相同的單詞只計數一次   
  79.                 }  
  80.             }  
  81.             return sum;  
  82.         }  
  83. };  
  84. /* AC_DFA  end */  
原文:http://blog.csdn.net/mobius_strip/article/details/22549517
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章