浅谈AC自动机

引入

我们发现\(Trie\)树可以进行多模式串匹配,\(KMP\)可以快速进行子串匹配,那么如果我们要进行多模式串子串匹配怎么办呢?这里我们就将介绍一个综合了\(Trie\)树和\(KMP\)的算法——\(AC\)自动机。

简述

上面已经提到了\(AC\)自动机就是\(Trie\)树和\(KMP\)的综合,如果还不会或者不熟悉这两种算法的话请移步初阶字符串算法

我们首先要建出一棵\(Trie\)树来,和普通的\(Trie\)树一样的。

代码如下:

void insert(char *s){
    int p=0,len=strlen(s);
    for(int i=0;i<len;i++){
        int ch=s[i]-'a';
        if(!trie[p][ch])trie[p][ch]=++tot;
        p=trie[p][ch];
    }ed[p]++;
}

然后我们就需要和\(KMP\)一样构建失配指针,也就是\(fail\)指针。而与\(KMP\)算法不同的是\(KMP\)算法的\(fail\)指针是相同的前后缀,而\(AC\)自动机只需要相同后缀就可以了。

那么我们应该如何构建呢?我们同样也是利用已经求得的\(fail\)指针来推导出当前节点的\(fail\)指针。我们采用\(BFS\)的方式来实现这个过程。我们设现在的节点为\(x\),它的父亲节点为\(fa_x\),它和它的父亲之间通过字符为\(ch\)的边连接,且深度小于\(x\)的所有节点的\(fail\)指针都已经求得。

  • 我们首先跳转到\(fail[fa_x]\)节点
    • 如果\(fail[fa_x]\)节点有一个子节点\(y\)是由该节点通过字符为\(ch​\)的边连接的
      • \(x\)\(fail\)指针指向\(y\),即\(fail[x]=y\)
    • 如果不存在这样的节点
      • 那么就跳转到\(fail[fa_x]\)\(fail\)指针指向的节点,即\(fail[fail[fa_x]]\),然后重复上述过程
      • 如果一直跳转到根节点依然没有满足条件的节点的话,就让\(x\)\(fail\)指针指向根节点,即\(fail[x]=root\)

上述过程便是\(fail\)指针的构建过程了。

对于字符串集合\(\{hers,his,she,i\}\)构建出来的\(fail\)指针如下:

代码如下:

void make_fail(){
    queue<int >q;
    memset(fail,0,sizeof(fail));
    for(int i=0;i<26;i++)if(trie[0][i])q.push(trie[0][i]);
    while(!q.empty()){
        int x=q.front();q.pop();
        for(int i=0;i<26;i++){
            if(trie[x][i]){
                fail[trie[x][i]]=trie[fail[x]][i];
                q.push(trie[x][i]);
            }else trie[x][i]=trie[fail[x]][i];
        }
    }
}

我们注意到上面的构建\(fail\)指针的代码中trie[x][i]=trie[fail[x]][i]这句话,为什么直接将\(fail[x]\)的子节点直接赋给\(x\)了呢?其实这行代码和上面两行的fail[trie[x][i]]=trie[fail[x]][i]共同做了类似于并查集的“路径压缩”的事情,也就是使得本身可能要跳转很多次的\(fail\)指针变成只需要跳转一次,这个可以感性理解一下。

匹配函数的代码如下:

int query(char *s){
    int p=0,ret=0,len=strlen(s);
    for(int i=0;i<len;i++){
        int ch=s[i]-'a';p=trie[p][ch];
        for(int j=p;j&&~ed[j];j=fail[j])ret+=ed[j],ed[j]=-1;
    }return ret;
}

\(p\)就是当前在\(Trie\)树上的节点,然后利用\(fail\)指针来找出所有匹配的模式串。但是我们发现一个问题,p=trie[p][ch]这行代码告诉我们\(p\)似乎是不断向后跳的,并没有像\(KMP\)一样跳到失配指针指向的节点。但其实并不然,我们看一下之前的构造\(fail\)指针的代码,我们发现,我们其实是对\(Trie\)树进行了改造的。所以,我们并不是不断向后跳的,我们同样的实现了跳\(fail\)指针的操作。

以上便是\(AC\)自动机的构造失配指针和匹配过程。

总代码如下:

struct AC_automaton{
    int tot,ed[maxn],fail[maxn],trie[maxn][26];
    void insert(char *s){
        int p=0,len=strlen(s);
        for(int i=0;i<len;i++){
            int ch=s[i]-'a';
            if(!trie[p][ch])trie[p][ch]=++tot;
            p=trie[p][ch];
        }ed[p]++;
    }
    void make_fail(){
        queue<int >q;
        memset(fail,0,sizeof(fail));
        for(int i=0;i<26;i++)if(trie[0][i])q.push(trie[0][i]);
        while(!q.empty()){
            int x=q.front();q.pop();
            for(int i=0;i<26;i++){
                if(trie[x][i]){
                    fail[trie[x][i]]=trie[fail[x]][i];
                    q.push(trie[x][i]);
                }else trie[x][i]=trie[fail[x]][i];
            }
        }
    }
    int query(char *s){
        int p=0,ret=0,len=strlen(s);
        for(int i=0;i<len;i++){
            int ch=s[i]-'a';
            p=trie[p][ch];
            for(int j=p;j&&~ed[j];j=fail[j])ret+=ed[j],ed[j]=-1;
        }return ret;
    }
}AC;
posted @ 2019-01-16 21:35  Luvwgyx  阅读(184)  评论(0编辑  收藏  举报