上篇文章(http://www.cnblogs.com/zzqcn/p/3508442.html)里提到的BF和KMP算法都是單模式串匹配算法,也就是說,模式串只有一個。當需要在字符串中搜索多個關鍵字(模式)時,則需要用到多模式串匹配算法。
簡介
AC(Aho-Corasick)算法是一個經典的多模式串匹配算法,它借鑒了KMP算法的思想,可以由有限狀態機(Finite State Automata:FSA)來表示。AC算法的基本原理是:
先根據多模式串建立一個有限狀態自動機FSA,在進行模式匹配時,設當前狀態為Scur,輸入串中的當前符號為C,運行FSA,試圖得到下一狀態Snext。如果Snext無效,即發生失配,則根據規則,將當前狀態回退到一個適合的狀態Sfail,然后重復上一過程,直到Snext有效或Scur為0。在匹配的過程中,如果當前狀態正好匹配了某個模式P,則輸出它。
由AC算法的原理,我們可以知道,用於匹配的FSA與輸入串無關,而只與模式串有關;匹配過程中如果發生失配,則FSA應回退到某一狀態,而輸入串指針無需回退。這兩點都與KMP算法的思想吻合。
要實現基本的AC算法,即實現FSA邏輯,需要構建3樣東西:
- 狀態跳轉表goto :決定對於當前狀態S和條件C,如何得到下一狀態S‘
- 失配跳轉表fail : 決定goto表得到的下一狀態無效時,應該回退到哪一個狀態
- 匹配結果輸出表output : 決定在哪個狀態時輸出哪個恰好匹配的模式
構造示例
原理比較抽象,下面以一個簡單而且經典的例子(來自於AC算法創始人的論文)來演示一下AC算法的原理及goto、fail、output這3個表的構建。設多模式為 {“he”, “she”, “his”, “hers”}。下面來一步一步構建FSA。
首先規定一個初始狀態0,接着依次處理多個模式串,首先是he:

每個圓圈代表一個狀態(State),圓圈中的數字表示狀態的編號;有向箭頭代表狀態的轉換,它由當前狀態指向下一狀態,箭頭上的字符表示此次狀態轉換的條件。
接下來是she,注意每處理一個模式串時,都要先回到起始狀態0:

注意,如果給定條件C,從當前狀態出發能轉換到一個有效狀態,那么只進行狀態轉換,而不創建新狀態,這一點在處理后面兩個模式串時可以看到。接着是his:

最后是hers,而且AC規定,當當前狀態為初始狀態(0)時,對於任意條件C來,都能轉換到有效狀態(這也避免了回退的死循環),因此,除了條件h和s外,對其他任意條件,狀態0還應轉換至狀態0:

其實,最后的FSA圖就表示了goto表,因為上面畫的狀態及狀態轉換都是有效的。另外,也可以自然地知道,每處理完一個模式時,FSA的當前狀態都對應着此模式。因此,狀態2,5,7,8分別對應模式he, she, his, hers,也就是2,5,7,8這4個狀態對應的output表中的值。不過,到目前為止output表還未創建完成。
在下面的討論中,我們用S’ = goto(S,C)表示狀態S經由條件C轉換到狀態S‘,fail(S)表示狀態S的fail表值,output(S)表示狀態S的output表值。
goto表反映了有效的狀態轉換。而在fail表反映了轉換失敗時應回退到的狀態。比如,設當前狀態為2,當條件不是r時,狀態轉換就失敗,必須回退到某一個合適的狀態,這個狀態就是狀態2的fail值。而fail值的求法呢,和KMP算法中求next函數一樣,可以用遞推法來進行。
首先規定與狀態0距離為1(即深度為1)的所有狀態的fail值都為0。然后設當前狀態是S1,求fail(S1)。我們知道,S1的前一狀態必定是唯一的(從FSA圖也可以看出),設S1的前一狀態是S2,S2轉換到S1的條件為C,測試S3 = goto(fail(S2), C),如果成功,則fail(S1) = goto(fail(S2), C),如果不成功,繼續測試S4 = goto(fail(S3), C)是否成功,如此重復,直到轉換到某個有效的狀態Sn,令fail(S1) = Sn。
做為例子,我們來求狀態3,4,5的fail值。首先,按照約定fail(3) == 0。接着求fail(4),由於goto(fail(3), h) == goto(0, h) == 1,所以fail(4) == 1。接着求fail(5),由於goto(fail(4), e) == goto(1, e) == 2,所以fail(5) == 2。在這里我們注意到,當FSA的狀態轉換到狀態5時,不僅匹配了she,而且匹配了he,這意味着對兩個狀態S和S’,S>S’,如果fail(S) == S‘,則output(S)應添加output(S’)。這樣一來,output表也構建完整了。fail表如下:
| 狀態 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| fail值 | N/A | 0 | 0 | 0 | 1 | 2 | 0 | 3 | 0 | 3 |
3個表構建完成后,就可以對輸入串進行模式匹配了。設輸入串為“ushers”,則匹配過程如下:
| 符號 | 狀態轉換 | 輸出 |
| u | 0->0 | |
| s | 0->3 | |
| h | 3->4 | |
| e | 4->5 | she he |
| r | 5->2 2->8 | |
| s | 8->9 | hers |
注意,雖然掃描到符號r時經歷了狀態2,但那只是一個中間狀態,之后立刻切換到狀態8,因此並不輸出output(2)的內容{he},這也避免了模式”he”的重復輸出。
至此,我們已經構建了goto,fail,output 3個必需的表。這時構建的FSA也叫做非確定性有限狀態機(Nondeterministic Finite State Automata: NFA),也就是說,當匹配過程中發生失配時,應根據fail表進行狀態回退,但具體回退到哪個狀態為止,是不確定的,需要一次或多次循環回溯。可以預見,NFA會影響模式匹配的效率。可以通過構建確定性有限狀態機(Deterministic Finite State Automata: DFA)來彌補這個缺陷,它的原理是,在當前狀態下,對於任意條件,都可以確定地給出下一狀態,而不需要進行循環。
設當前狀態是S,條件為C,問題是如何求得下一個確定狀態S‘。如果goto(S, C)成功,則S’ = goto(S, C);否則,令S‘ = goto(fail(S), C),如果S’有效且不為0,則S‘就是那個確定狀態,此時,應把S’ = goto(S, C)這個關系添加到goto表中。可以預見,DFA會提高匹配速度,但由於向goto表添加了更多的條目,會導致存儲消耗增加。
做為例子,我們來求狀態1的確定狀態{S’}。首先fail(1)==0。當C==e或i時,S‘不變,分別是2,6。當C==h時,由於goto(0,h)==1,所以S’==1。當C==s時,由於goto(0, s)==3,所以S’==3。對於其他條件C,goto(0, C)==0。因此,我們需要往之前構建的goto表添加兩項:1=goto(1, h), 3=goto(1,s)。
C++簡單實現
下面,用簡單的C++代碼來實現前文所述的AC算法,包括NFA和DFA形式。
簡單介紹一下宏定義、數據結構和各函數的作用:
| 名稱 | 類型 | 說明 |
| AC_FAIL_STATE | 宏 | 無效/失敗的跳轉狀態 |
| AC_UNDEF_FAIL | 宏 | 未定義的fail表值 |
| AC_CONT_SIZE | 宏 | 所有可能的條件,如果處理char字符,則范圍是0-255 |
| ac_transition | 結構 | goto表項,不含當前狀態,因為它等於數組下標 |
| FSAType | 枚舉 | FSA類型,fail表創建后變為NFA,然后可以轉換為DFA |
| ac_state_finder | 函數對象 | 用於查找當前狀態下符合輸入條件的goto表項 |
| ac_goto | 函數 | 實現S'=goto(S, C)邏輯,用於goto表創建和模式匹配過程 |
| ac_creat_goto_table | 函數 | 創建goto表 |
| ac_creat_fail_table | 函數 | 創建fail表,完成后將實現NFA |
| ac_convert_to_DFA | 函數 | 將NFA轉換為DFA。會向goto表添加新項。完成后不再需要fail表 |
| ac_print_goto_table | 函數 | 打印所有狀態及其對應的轉換表、fail值(僅限NFA)、output值 |
| ac_search | 函數 | AC搜索函數 |
還有,NFA和DFA的創建過程中,需要用到類似圖的廣度優先搜索(Breadth-First-Search:BFS)算法,因此用到了隊列(std::queue)容器。
1 // Author: 趙子清 2 // Blog: http://www.cnblogs.com/zzqcn 3 4 #include <vector> 5 #include <queue> 6 #include <algorithm> 7 #include <functional> 8 #include <string> 9 #include <cstdio> 10 using namespace std; 11 12 13 #define AC_FAIL_STATE -1 14 #define AC_UNDEF_FAIL -1 15 #define AC_CONT_SIZE 256 16 17 18 struct ac_transition 19 { 20 char condition; 21 int next; 22 }; 23 24 enum FSAType 25 { 26 FSA_NULL = 0, 27 FSA_NFA, 28 FSA_DFA 29 }; 30 31 32 struct ac_state_finder : 33 public binary_function<ac_transition, char, bool> 34 { 35 bool operator() (const ac_transition& t, char c) const 36 { 37 return (t.condition == c); 38 } 39 }; 40 41 42 vector<vector<ac_transition>> goto_table; 43 vector<vector<string>> output_table; 44 vector<int> fail_table; 45 FSAType fsa_type = FSA_NULL; 46 47 48 int ac_goto(int _state, char _cont); 49 int ac_creat_goto_table(const vector<string>& _ptns); 50 int ac_creat_fail_table(); 51 int ac_convert_to_DFA(); 52 int ac_print_goto_table(); 53 int ac_search(const string& _txt); 54 55 56 int main(int argc, char** argv) 57 { 58 string ss[4] = {"he", "she", "his", "hers"}; 59 vector<string> ptns(ss, ss+4); 60 string txt = "ushers"; 61 62 ac_creat_goto_table(ptns); 63 ac_creat_fail_table(); 64 ac_print_goto_table(); 65 ac_search(txt); 66 67 ac_convert_to_DFA(); 68 ac_print_goto_table(); 69 ac_search(txt); 70 71 return 0; 72 } 73 74 75 int ac_goto(int _state, char _cont) 76 { 77 vector<ac_transition>::const_iterator ret = 78 find_if(goto_table[_state].begin(), goto_table[_state].end(), 79 bind2nd(ac_state_finder(), _cont)); 80 if(goto_table[_state].end() == ret) 81 { 82 if(0 == _state) 83 return 0; 84 else 85 return AC_FAIL_STATE; 86 } 87 else 88 return ret->next; 89 } 90 91 92 int ac_creat_goto_table(const vector<string>& _ptns) 93 { 94 int state = 0; 95 int state_id = 0; 96 97 ac_transition t; 98 vector<ac_transition> ts; 99 vector<string> ss; 100 101 goto_table.push_back(ts); 102 output_table.push_back(ss); 103 state_id++; 104 105 for(vector<string>::const_iterator i = _ptns.begin(); i != _ptns.end(); ++i) 106 { 107 state = 0; 108 for(string::const_iterator j=i->begin(); j<i->end(); ++j) 109 { 110 int next_state = ac_goto(state, *j); 111 if(0 == next_state || AC_FAIL_STATE == next_state) 112 { 113 t.condition = *j; 114 t.next = state_id++; 115 goto_table[state].push_back(t); 116 117 goto_table.push_back(ts); 118 output_table.push_back(ss); 119 120 state = t.next; 121 } 122 else 123 state = next_state; 124 } 125 output_table[state].push_back(*i); 126 } 127 128 return 0; 129 } 130 131 132 int ac_creat_fail_table() 133 { 134 if(goto_table.empty()) 135 return -1; 136 137 fail_table.resize(goto_table.size()); 138 for(size_t i=0; i<goto_table.size(); ++i) 139 fail_table[i] = AC_UNDEF_FAIL; 140 141 queue<int> q; 142 for(vector<ac_transition>::const_iterator i = goto_table[0].begin(); 143 i != goto_table[0].end(); ++i) 144 { 145 fail_table[i->next] = 0; 146 q.push(i->next); 147 } 148 149 int state; 150 while(!q.empty()) 151 { 152 state = q.front(); q.pop(); 153 154 for(vector<ac_transition>::const_iterator i = goto_table[state].begin(); 155 i != goto_table[state].end(); ++i) 156 { 157 if(AC_UNDEF_FAIL != fail_table[i->next]) 158 continue; 159 160 q.push(i->next); 161 162 int prev_state = state, ret; 163 do 164 { 165 prev_state = fail_table[prev_state]; 166 ret = ac_goto(prev_state, i->condition); 167 } while (AC_FAIL_STATE == ret); 168 169 fail_table[i->next] = ret; 170 171 for(vector<string>::const_iterator j = output_table[ret].begin(); 172 j != output_table[ret].end(); ++j) 173 { 174 vector<string>::const_iterator sret = 175 find(output_table[i->next].begin(), output_table[i->next].end(), *j); 176 if(output_table[i->next].end() == sret) 177 output_table[i->next].push_back(*j); 178 } 179 } 180 } 181 182 fsa_type = FSA_NFA; 183 184 return 0; 185 } 186 187 188 int ac_convert_to_DFA() 189 { 190 if(fsa_type != FSA_NFA) 191 return -1; 192 193 if(goto_table.empty() || fail_table.empty()) 194 return -1; 195 196 queue<int> q; 197 for(vector<ac_transition>::const_iterator i = goto_table[0].begin(); 198 i != goto_table[0].end(); ++i) 199 { q.push(i->next); } 200 201 int state; 202 while(!q.empty()) 203 { 204 state = q.front(); q.pop(); 205 206 for(size_t c=0; c<AC_CONT_SIZE; ++c) 207 { 208 int next_state = ac_goto(state, c); 209 if(next_state != AC_FAIL_STATE && next_state != 0) 210 q.push(next_state); 211 else 212 { 213 next_state = ac_goto(fail_table[state], c); 214 if(next_state != AC_FAIL_STATE && next_state != 0) 215 { 216 ac_transition t; 217 t.condition = c; 218 t.next = next_state; 219 goto_table[state].push_back(t); 220 } 221 } 222 } 223 } 224 225 fail_table.clear(); 226 fsa_type = FSA_DFA; 227 228 return 0; 229 } 230 231 232 void OutputMatch(int _state, size_t _pos) 233 { 234 for(vector<string>::const_iterator i = output_table[_state].begin(); 235 i != output_table[_state].end(); ++i) 236 { 237 printf("%d %s : %d\n", _state, i->c_str(), _pos - i->length()); 238 } 239 } 240 241 int ac_search(const string& _txt) 242 { 243 if(goto_table.empty() || FSA_NULL == fsa_type) 244 return -1; 245 246 int state = 0; 247 string::size_type i; 248 for(i=0; i<_txt.length(); ++i) 249 { 250 char c = _txt[i]; 251 if(output_table[state].size() > 0) 252 OutputMatch(state, i); 253 254 if(FSA_NFA == fsa_type) 255 { 256 while(AC_FAIL_STATE == ac_goto(state, c)) 257 state = fail_table[state]; 258 state = ac_goto(state, c); 259 } 260 else if(FSA_DFA == fsa_type) 261 { 262 state = ac_goto(state, c); 263 if(AC_FAIL_STATE == state) 264 state = 0; 265 } 266 } 267 268 if(output_table[state].size() > 0) 269 OutputMatch(state, i); 270 271 return 0; 272 } 273 274 275 int ac_print_goto_table() 276 { 277 if(goto_table.empty()) 278 return -1; 279 280 int state_id = 0; 281 for(vector<vector<ac_transition>>::const_iterator i = goto_table.begin(); 282 i != goto_table.end(); ++i, ++state_id) 283 { 284 printf("%d: ", state_id); 285 286 if(FSA_NFA == fsa_type) 287 printf("%d ", fail_table[state_id]); 288 289 for(vector<ac_transition>::const_iterator j = i->begin(); 290 j != i->end(); ++j) 291 { 292 printf("%c->%d ", j->condition, j->next); 293 } 294 295 for(vector<string>::const_iterator j = output_table[state_id].begin(); 296 j != output_table[state_id].end(); ++j) 297 { 298 printf("(%s) ", j->c_str()); 299 } 300 printf("\n"); 301 } 302 printf("\n"); 303 304 return 0; 305 }
輸出結果:
0: -1 h->1 s->3 1: 0 e->2 i->6 2: 0 r->8 (he) 3: 0 h->4 4: 1 e->5 5: 2 (she) (he) 6: 0 s->7 7: 3 (his) 8: 0 s->9 9: 3 (hers) 5 she : 1 5 he : 2 9 hers : 2
0: h->1 s->3 1: e->2 i->6 h->1 s->3 2: r->8 h->1 s->3 (he) 3: h->4 s->3 4: e->5 h->1 i->6 s->3 5: h->1 r->8 s->3 (she) (he) 6: s->7 h->1 7: h->4 s->3 (his) 8: s->9 h->1 9: h->4 s->3 (hers) 5 she : 1 5 he : 2 9 hers : 2
參考資料:
【1】《Efficient String Matching: An Aid to Bibliographic Search》 - Alfred V. Aho & Margaret J. Corasick, 貝爾實驗室
