徹底理解AC多模式匹配算法


(本文尤其適合遍覽網上的講解而仍百思不得姐的同學) 

一、原理

AC自動機首先將模式組記錄為Trie字典樹的形式,以節點表示不同狀態,邊上標以字母表中的字符,表示狀態的轉移。根節點狀態記為0狀態,表示起始狀態。當一個狀態處有一個模式串終結則標記一下。

目前流傳較多的講解多大同小異,尤其是配圖,基本采用的是Aho和Corasiek兩位巨巨的文章efficient string matching an aid to bibliographic search里的,竊以為那張示意圖存在失配點靠前的特點(什么是失配?往下看),看起來稍稍費勁。

我找了樣例畫了一套新圖,主要目標是通過稍微的誇張(失配點遠離)讓過程更清晰。

匹配的過程是:從0狀態起點開始,以字符流輸入,進行適當的狀態轉移,如果可以抵達某一標記終結的狀態,則成功匹配模式,串值為從0到終結點的路徑。

按照傳統的說法,狀態機有三個主要函數支撐:goto(狀態正常轉移),fail(狀態失配轉移),output(傳回匹配結果),而我認為與其規定是具體的函數,倒不如說是三個功能的模塊,有不同於函數的實現形式。

 

Trie樹的建立是簡單的,在此基礎上我們要完善更多的數據結構,實現goto的功能。

goto是自動機基本的狀態轉移過程,很好想,就是在建立Trie樹時讓每個狀態維護一組指針(廣義的),使得在每一狀態對於輸入都可以正確轉移,沒有對應的則報錯(現在回答剛才的問題,什么是失配?失配就是一個狀態接受了無法轉移的字符,記fail)。除了字典樹中的樹枝以外,還有一個轉移就是在開始節點,對於不能流進自動機的字符,不報錯而是再一次轉到開始節點(如上圖示),很好理解,對於待匹配串λthis,λ為不含t,h的任意串,真正的模式匹配是在去除了它以后開始的。(當然還有其他的用意,坑稍后填)

 

好了,正常的狀態流轉已經建立好了,現在看失配時我們的狀態流何去何從。舉一個栗子,如果輸入thip這個串,狀態的流轉應該如下圖:

那3狀態處報錯后應該怎么處理呢?最好想的方法當然是錯開一位,再從頭開始匹配(這種方法就像一位老人家曾經說過,太年輕太簡單,有時還很幼稚),AC二位的辦法是——利用圖中的關系計算出一套跳轉關系——在x點處失配的串不打回開頭來過,而是跳到y點——繼續匹配當前字符。這套規則叫做失配函數,也就是fail功能模塊。要點在於當前字符不向前回溯,想想着很適合字符流的關鍵字匹配對不對。

好了,告訴大家3狀態的失配跳轉在6狀態,先不用管怎么得到的,想想這個過程:3得到p字符,失配,憑goto無法轉移狀態,使用失配時通用的fail,狀態跳至6,接受p——還是這個字符,成功匹配到終結狀態7。單趟遍歷目標串,cool。

當然這套規則是需要小心計算的。采用的方法很巧妙,在樹形結構中很像廣度優先搜索BFS,數學形式又很像動態規划DP。

正式開始之前請認真思考這個情況:已知2狀態的失配跳轉為5,怎么求3狀態的失配跳轉?從圖中很容易看出,2通過i流向3,而5恰有對i的goto,自然地,3失配時可以跳轉至6,噠噠

現在我把圖小小地改動一下,把hip變成hop,我們都喜歡hip-hop~:

 

2的失配跳轉仍然是5,然而對於所有使2不失配的字符,5都沒有合適的goto——即會在5也失配,此種情況怎么求3的失配跳轉?

請仔細讀這兩句話:

2的失配跳轉說明不能采用前綴th

5的失配跳轉說明不能采用前綴h(現在不要想2的事情了,單獨想5)

——失配跳轉實際上是一個逐字符推后匹配模式前綴的過程

 那么既然h開頭的也不能匹配模式了,那么對於目標串,要從i開始匹配了——下一次狀態就是5的失配跳轉0!

這是一個向前遞歸的過程,而前面提到0的大量無匹配字符均指回0自己則巧妙地保證了這個遞歸會最不濟也在會0處停下:這種時候則是放棄之前的全部前綴從當前字符重新開始嘗試匹配了,對吧。

我要強調,失配跳轉的過程中當前字符是不變的。

至此,我們也完整的構造了fail模塊的規則。

output需要做的則是對匹配路徑上的每一狀態,檢查是否為一個模式的終結,如果是,用一種合適的方式傳回這個匹配的結果。

Another question!目標串全部模式匹配:在匹配到一個模式后,應當驅動自動機繼續無遺漏地匹配下一個出現的模式(這個下一模式也許會和已匹配的部分或全部重疊),我再次重復這句話,失配跳轉實際上是一個逐字符推后匹配模式前綴的過程,那么應該很簡單了吧,匹配到一個模式后自然一次失配跳轉就行了!自動機會把前綴去掉一個字符繼續匹配。

 

二、關於自動機的數據結構表示

我在原理中避開一個一定要解釋清楚的問題,就是自動機的數據結構實現。Aho & Corasiek的論文中稱為goto/fail/output function,與其理解為函數倒不如說是功能,因為它們的實現不必是有輸入輸出的函數,而可以是向更直接的數據結構直接查詢。

我實踐中認為易於實現的寫法:goto功能就可以實現在結點結構中,每個狀態維護一個轉向結點的指針,無效則置空;fail即可以是一張自動機維護的表;output在結點中標記是否終結,如果終結,狀態結點存儲模式串,檢測到終結直接傳回。

 

三、完整代碼

  1 #include <cstdlib>
  2 #include <set>
  3 #include <string>
  4 #include <vector>
  5 #include <queue>
  6 #include <iostream>
  7 
  8 using namespace std;
  9 
 10 #define ALPHABET_NUMBER 26
 11 
 12 struct StateNode
 13 {
 14     bool finish_{ false };
 15     int state_{ 0 };
 16     string pattern_{};
 17     //goto table
 18     vector<StateNode *> transition_table_{ vector<StateNode *>(ALPHABET_NUMBER) };
 19 };
 20 
 21 class ACSM
 22 {
 23 private:
 24     StateNode *start_node_;
 25     int state_count_;
 26     vector<StateNode *> corresponding_node_;
 27     vector<StateNode *> fail_;
 28 public:
 29     ACSM() :start_node_{ new StateNode() }, state_count_{ 0 }
 30     {
 31         //state0 is start_node_
 32         corresponding_node_.push_back(start_node_);
 33     }
 34     //read all patterns and produce the goto table
 35     void load_pattern(const vector<string> &_Patterns)
 36     {
 37         int latest_state = 1;
 38         for (const auto &pattern : _Patterns)
 39         {
 40             auto *p = start_node_;
 41             for (int i = 0; i < pattern.size(); ++i)
 42             {
 43                 auto *next_node = p->transition_table_[pattern[i] - 'a'];
 44                 if (next_node == nullptr)
 45                 {
 46                     next_node = new StateNode();
 47                 }
 48                 if (next_node->state_ == 0)
 49                 {
 50                     next_node->state_ = latest_state++;
 51                     //update the table
 52                     corresponding_node_.push_back(next_node);
 53                 }
 54                 //the goto table
 55                 p->transition_table_[pattern[i] - 'a'] = next_node;
 56                 p = next_node;
 57             }
 58             p->finish_ = true;
 59             p->pattern_ = pattern;
 60         }
 61         for (int i = 0; i < ALPHABET_NUMBER; ++i)
 62         {
 63             if (start_node_->transition_table_[i] == nullptr)
 64             {
 65                 start_node_->transition_table_[i] = start_node_;
 66             }
 67         }
 68         state_count_ = latest_state;
 69     }
 70     //produce fail function
 71     void dispose()
 72     {
 73         queue<StateNode *> q;
 74         fail_ = std::move(vector<StateNode *>(state_count_));
 75         for (const auto nxt : start_node_->transition_table_)
 76         {
 77             //d=1,f=0
 78             if (nxt->state_ != 0)
 79             {
 80                 fail_[nxt->state_] = start_node_;
 81                 q.push(nxt);
 82             }
 83         }
 84         //calculate all fail redirection
 85         while (!q.empty())
 86         {
 87             auto known = q.front();
 88             q.pop();
 89             for (int i = 0; i < ALPHABET_NUMBER; ++i)
 90             {
 91                 auto nxt = known->transition_table_[i];
 92                 if (nxt && nxt->state_ != 0)
 93                 {
 94                     auto p = fail_[known->state_];
 95                     while (!p->transition_table_[i])
 96                     {
 97                         p = fail_[p->state_];
 98                     }
 99                     fail_[nxt->state_] = p->transition_table_[i];
100                     q.push(nxt);
101                 }
102             }
103         }
104     }
105     //search matching
106     void match(const string &_Str, set<string> &_S)
107     {
108         auto p = start_node_;
109         for (int i = 0; i < _Str.size(); ++i)
110         {
111             int trans = _Str[i] - 'a';
112             p =
113                 p->transition_table_[trans]
114                 ? p->transition_table_[trans]
115                 : (--i, fail_[p->state_]);
116             if (p->finish_)
117             {
118                 _S.insert(p->pattern_);
119             }
120         }
121     }
122 };
123 
124 int main()
125 {
126     ACSM acsm;
127     vector<string> patterns{ "his","hers","she","he" };
128     set<string> matched;
129     acsm.load_pattern(patterns);
130     acsm.dispose();
131     string str{ "hishers" };
132     acsm.match(str, matched);
133     for (const auto str : matched)cout << str << endl;
134     system("pause");
135     return 0;
136 }

 

謝謝閱讀


免責聲明!

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



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