Aho-Corasick自动机浅析

4

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

你可能感兴趣的

rockeet · 2014年12月16日

多正则引擎
AC 自动机解决了匹配多个精确 term 的问题。
多正则引擎解决的是匹配多个正则表达式的问题。
理想情况下 AC 自动机匹配过程中碰到 term 的结尾时触发动作:告诉调用者,在这里发现了完整匹配,能匹配的 term id 是 {id1,id2...} 。
多正则引擎对正则表达式做同样的事情。

+1 回复

Ph智 · 2014年09月10日

学习了!点赞

回复

fionser · 2018年03月12日

AC 自动机 1975 年。(K)MP 1970 年。所以说 AC 自动机 是 KMP 的一种 extension 是没问题的。

回复

载入中...