"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; }