AC自动机 (Aho-Corasick) 浅显易懂
阅读原文时间:2021年04月20日阅读:1

咸鱼:你听过AC自动机吗?

不想吃WA的咸鱼:AC自动机,什么魔鬼?一脸茫然.jpg

咸鱼:AC自动机你不知道吗,就是一个算法模板,把这个模板提交到VJ上的任何题目都能自动AC。

不想吃WA的咸鱼:哇!好厉害,赶紧来学一手,这样我就能A掉VJ上的所有题还不吃WA。

在模式匹配问题中,如果模式串有多个,KMP算法就不合适了,因为每次要匹配一个模式串,就要遍历一遍文本串。我们想到KMP算法的状态转移图是利用前缀函数加失配的边匹配上的,如果我们把多个模式串也仿造KMP算法,构建为一个状态转移图不就行了吗,这就是著名的AC自动机了。那为了把所有的的模式串都存在一个状态图里,这时候字典树就排上用场了。注意到KMP的状态转移图是线性的字符串加上失配边组成的,可以猜到AC自动机是Trie加上失配的边组成的。

下图就是字符串{"SHE","HIS","HERS","FG"}所对应的Aho-Corasick自动机

从上图我们不难看出其实AC自动机还是在找自己的真后缀与其他模式串的前缀的过程,比如已经匹配上了字符串“SHE”,那么“HE”肯定也是能匹配上的,所以有一条91指向76的边而且为绿色,代表成功构建了失配指针。通俗一点来讲是这样的比如文本串T=“SHERS”,那么最先匹配上的依次是S,H,E,但是下一个R就匹配不上了,但是文本串里面可能有其他的模式串,而且那个模式串是没有匹配上的字符串"SHE"的子集“HE”,要是后面文本串出现的字符串能与这个子集字符串后面的字符串匹配上,就成功找到了一个模式串,这其实和KMP算法的核心思想是一样的,既然前面能匹配上,那么他的子集肯定能匹配上,接下来就看他的子集能不能和接下来的文本串中的字符匹配上,直到子集为空,指向根结点。

AC 自动机算法分为 3 步,1:构造一棵 Trie 树,2:构造失配指针,3:模式匹配过程。 

失配指针相当于KMP算法中的 next[ ],图中的虚线就是失配指针。

构造失配指针的过程概括起来就一句话:“设这个结点上的字母为 C,沿着他父亲的失配指针走直到走到一个结点他的儿子中也有字母为 C 的结点。然后把当前结点的失配指针指向那个字母也为C的儿子。如果一直走到了 root 都没找到,那就把失败指针指向 root。” 失配指针便是 AC 自动机算法中的精髓。究其根本失配指针与 KMP 中的 next[ ]数组的功能是一样的。 

#include<cstdio>
#include<cstring>
using namespace std;
const int maxp=50+7;
const int maxt=1000000+7;
const int maxq=10000*maxp;
struct Trie{                   //利用结构体来封装字典树的结点
    Trie* next[26];          
    Trie* fail;
    int num;
    Trie(){                  //构造函数 便于初始化
        for(int i=0;i<26;i++)
        next[i]=NULL;
        fail=NULL;
        num=0;
    }
};
char P[maxp];
char T[maxt];
Trie* q[maxq];
void insert(Trie* root,char* s)      //在字典树上 插入字符串s
{
    Trie* p=root;
    for(int i=0;s[i]!='\0';i++){
        if(p->next[s[i]-'a']==NULL){
            p->next[s[i]-'a']=new Trie;
        }
        p=p->next[s[i]-'a'];
    }
    p->num++;
}
void build_ac_automation(Trie* root)   //利用BFS创建AC自动机
{
    int head=0,tail=0;
    q[tail++]=root;
    while(head!=tail){
        Trie* front=q[head++];
        for(int i=0;i<26;i++){         //遍历队头元素的子结点
            if(front->next[i]!=NULL){
                Trie* p=front->fail;
                while(p!=NULL){                //只有根结点的失配指针为 NULL
                    if(p->next[i]!=NULL){       顺着失配指针往回走,直至某个节点,
                        front->next[i]->fail=p->next[i];//其拥有一个字母为'a'+i的子结点
                        break;
                    }
                    p=p->fail;     // 沿着失配指针一直找
                }
                if(p==NULL) front->next[i]->fail=root; 
                    //p==NULL 说明顺着失配指针往回走的过程中没有找到合适的结点
                q[tail++]=front->next[i];
            }
        }
    }
}
int ac_find(Trie* root,char* T)
{
    int ans=0;
    Trie* p=root;
    for(int i=0,len=strlen(T);i<len;i++){
        while(p->next[T[i]-'a']==NULL&&p!=root)  //若当前结点的没有一个字符为 T[i]的儿子且当前不是根节点 
            p=p->fail;          //通俗的讲,就是顺着失配指针往回走,直至找到合适的节点或根结点为止。
        if(p->next[T[i]-'a']!=NULL) p=p->next[T[i]-'a'];
        Trie* temp=p;
        while(temp!=root&&temp->num!=-1){   //顺着失配指针往回走,一直到根结点。
            ans+=temp->num;//若当前节点的num不为 0,则说明以当前节点字母结尾的单词出现过一次
                             //此单词是以上一次循环的结点单词为结尾的单词的子集。
            temp->num=-1;     //标记 num 为-1,避免重复计算
            temp=temp->fail;
        }
    }
    return ans;
}
int main()
{
    int t,n;
    scanf("%d",&t);
    while(t--){
        Trie* root=new Trie;
        scanf("%d",&n);
        getchar();
        for(int i=0;i<n;i++){
            gets(P);
            insert(root,P);
        }
        build_ac_automation(root);
        gets(T);
        printf("%d\n",ac_find(root,T));
    }
    return 0;
}

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章