Aho-Corasick自動機淺析

"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;
}
相關文章
相關標籤/搜索