"AC自動機不是隨便yy三分鐘就搞定的算法麼?"某犇如是說。蒟蒻MX默默流下了眼淚……html
因爲各類機緣巧合和本人的智力因素,我在離開OI一年多後,終於搞清楚了AC自動機(Aho-Chorasick string match algorithm)。網絡上介紹AC自動機的算法可能是藉助KMP算法(Knuth-Morris-Pratt algorithm)中的失配數組來寫,但明明AC自動機是先於KMP的,所以我決定徹底扔掉和KMP相關的東西,寫一篇像我這樣理解力比較低下的同窗也能看的懂的AC自動機算法講解。
在講述AC自動機以前,先簡單講講自動機是什麼。自動機是計算理論的一個概念,實際上是一張「圖」,每一個點是一個「狀態」,而邊則是狀態之間的轉移,根據條件能指導從一個狀態走向另外一個狀態。不少字符串匹配算法都是基於自動機模型的,好比被普遍使用的正則表達式。node
AC自動機算法算是比較簡單直觀的字符串匹配自動機,它其實就是在一顆Trie樹上建一些失配指針,當失配時只要順着失配指針走,就能避免一些重複的計算。好比對於字符串antibody和tide,若是第一個串匹配到第5個字符(b)失配了能夠直接走入第二個串的第3個字符(d)進行匹配,由於前面的「ti」是公共的,若是能匹配到第一個串的第5個字符,那麼前面兩個確定是ti。正則表達式
因此AC自動機分爲三部分:
1.建Trie樹
2.在Trie樹上創建失配指針,成爲AC自動機
3.自動機上匹配字符串算法
首先咱們先建構AC自動機的數據結構。既然基礎是Trie樹,咱們就用樹的結構描述它。本文程序均用C++編寫。如下是Trie樹節點的結構體:數組
struct node { node *fail; //失配指針 node *child[CHAR_SET_SIZE]; //兒子節點 int point; //標識這是第幾個模式串的停止節點 node(){ fail=NULL; for (int i=0;i<CHAR_SET_SIZE;++i) child[i]=NULL; point=-1; //非模式串停止節點,用-1表達 } };
而後就是Trie樹的插入,其實也是很簡單、很模板的:網絡
void Insert(char *s,int num) { node *p=Root; for (char *c=s;*c!='\0';++c) { int t=(*c)-'a'; if (p->child[t]==NULL) { p->child[t]=new node; } p=p->child[t]; if ((*(c+1))=='\0') p->point=num; } }
AC自動機的精髓在於失配指針!失配指針的構建方法是這樣的:對於一個節點C,標識字符a,順着C的父親節點的失配指針走,走到第一個有兒子也是a的節點T那麼C的失配指針就指向T的標識a的兒子節點。若是找不到這個節點,那麼失配指針指向Root。在實際操做時,是用廣搜來實現,由於這個建構過程要求父親節點的失配指針已經建好,並且一層層都要建好。代碼以下:數據結構
void BuildFailPoint() { int Qh=0,Qt=1; Q[1]=Root; while (Qh<Qt) { node *now=Q[++Qh]; for (int i=0;i<CHAR_SET_SIZE;++i) { if (now->child[i]!=NULL) { if (now==Root) now->child[i]->fail=Root; else { node *p=now->fail; while (p!=NULL) { if (p->child[i]!=NULL) { now->child[i]->fail=p->child[i]; break; } p=p->fail; } if (p==NULL) now->child[i]->fail=Root; } Q[++Qt]=now->child[i]; } } } }
這樣,AC自動機就建構完成了!如今對查詢串進行匹配。匹配過程當中,須要一個p指針指向上一步成功匹配的節點。若是當前字符c失配,則p要沿着本身的失配指針走,直到新的p有一個兒子標識c,若是走不到,嘿嘿,已經回到根了。由於有可能會同時匹配多個串,因此須要掃一遍全部能夠匹配的串。代碼以下:ide
vector <pair <int,int> > Query() { vector <pair <int,int> > Ret; //查詢返回值是第幾個模式串在什麼位置成功匹配 int Len=strlen(QueryString); node *p=Root; for (int i=0;i!=Len;++i) { int index=QueryString[i]-'a'; while (p->child[index]==NULL && p!=Root) p=p->fail; if (p->child[index]==NULL) continue; p=p->child[index]; node *t=p; while (t!=Root) //掃全部能夠匹配的串 { if (t->point!=-1) Ret.push_back(make_pair(t->point,i)); t=t->fail; } } return Ret; }
AC自動機核心的部分就這麼多,我寫了一個完整的AC自動機模板程序放在最後,僅供參考,如需使用該程序,請自便,無需告知我。ui
本文參考了:
* 《AC自動機算法詳解》 極限定理
* 《Aho–Corasick string matching algorithm》 Wikipedia
* 《正則指引》,餘晟著,電子工業出版社指針
#include <cstdio> #include <cstring> #include <cstdlib> #include <string> #include <vector> #include <utility> using std::string; using std::vector; using std::pair; using std::make_pair; #define CHAR_SET_SIZE 26 #define PATTERN_SIZE 300 #define QUERY_SIZE 3000 #define QSIZE 300000 struct node { node *fail; node *child[CHAR_SET_SIZE]; int point; node(){ fail=NULL; for (int i=0;i<CHAR_SET_SIZE;++i) child[i]=NULL; point=-1; } }; node *Q[QSIZE]; node *Root; vector <string> Pattern_Collection; void Init() { Root=new node; } void Insert(char *s,int num) { node *p=Root; for (char *c=s;*c!='\0';++c) { int t=(*c)-'a'; if (p->child[t]==NULL) { p->child[t]=new node; } p=p->child[t]; if ((*(c+1))=='\0') p->point=num; } } void InputPattern() { printf("Input number of patterns:"); fflush(stdout); int N; scanf("%d",&N); char s[PATTERN_SIZE]; for (int i=1;i<=N;++i) { scanf("%s",s); Pattern_Collection.push_back(s); Insert(s,Pattern_Collection.size()-1); } } void BuildFailPoint() { int Qh=0,Qt=1; Q[1]=Root; while (Qh<Qt) { node *now=Q[++Qh]; for (int i=0;i<CHAR_SET_SIZE;++i) { if (now->child[i]!=NULL) { if (now==Root) now->child[i]->fail=Root; else { node *p=now->fail; while (p!=NULL) { if (p->child[i]!=NULL) { now->child[i]->fail=p->child[i]; break; } p=p->fail; } if (p==NULL) now->child[i]->fail=Root; } Q[++Qt]=now->child[i]; } } } } char QueryString[QUERY_SIZE]; vector <pair <int,int> > Query() { vector <pair <int,int> > Ret; int Len=strlen(QueryString); node *p=Root; for (int i=0;i!=Len;++i) { int index=QueryString[i]-'a'; while (p->child[index]==NULL && p!=Root) p=p->fail; if (p->child[index]==NULL) continue; p=p->child[index]; node *t=p; while (t!=Root) { if (t->point!=-1) Ret.push_back(make_pair(t->point,i)); t=t->fail; } } return Ret; } void InputQuery() { printf("Input the query string:\n"); scanf("%s",QueryString); vector < pair <int,int> > QueryAns=Query(); for (int i=0;i!=QueryAns.size();++i) { printf("Found pattern \"%s\" at %d\n", Pattern_Collection[QueryAns[i].first].c_str(), QueryAns[i].second-Pattern_Collection[QueryAns[i].first].size()+1); } } int main() { Init(); InputPattern(); BuildFailPoint(); InputQuery(); return 0; }