"AC自动机不是随便yy三分钟就搞定的算法么?"某犇如是说。蒟蒻MX默默流下了眼泪……
由于各种机缘巧合和本人的智力因素,我在离开OI一年多后,终于搞清楚了AC自动机(Aho-Chorasick string match algorithm)。网络上介绍AC自动机的算法多是借助KMP算法(Knuth-Morris-Pratt algorithm)中的失配数组来写,但明明AC自动机是先于KMP的,因此我决定完全扔掉和KMP相关的东西,写一篇像我这样理解力比较低下的同学也能看的懂的AC自动机算法讲解。
在讲述AC自动机之前,先简单讲讲自动机是什么。自动机是计算理论的一个概念,其实是一张“图”,每个点是一个“状态”,而边则是状态之间的转移,根据条件能指导从一个状态走向另一个状态。很多字符串匹配算法都是基于自动机模型的,比如被广泛使用的正则表达式。
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,如果走不到,嘿嘿,已经回到根了。因为有可能会同时匹配多个串,所以需要扫一遍所有可以匹配的串。代码如下:
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自动机模板程序放在最后,仅供参考,如需使用该程序,请自便,无需告知我。
本文参考了:
* 《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;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。