AC自动机带你步步理解

AC自动机,听起来好像还不错可以帮你自动WA AC


AC自动机也就是Aho-Corasickautomation,产生于贝尔实验室
是一种多模式匹配算法(什么是多模式匹配,多模式匹配即为多个子串与母串匹配,相反单模式匹配就是单个子串与母串匹配,那么这就要用到臭名昭著的KMP算法,有兴趣可以下回讲解),建立在Trie图上及KMP思想上(实际与KMP关联不大)

好的上面看看就可以


首先我们先说明一下AC自动机大致流程,就是为Trie图加上一个指向与这条路径的后缀相同的前缀或直接一个路径的指针(也就是意味着如果当前这条路径可以走通,那么那一条路径也一定可以),让后再利用Trie图的查找这一功能,去匹配母串,再在匹配过程中直接利用该节点所对应指针指向的节点来进行求解.什么意思呢?不懂没关系,这只是大致了解过程,现在让我们通过详细求解一道题来分析


我们举一个很经典的例子,有一篇长度为m的文章,现在给出n个单词,问其中出现几个。(n<=106,m<=106,单词长度t<=10^6)那么这个如果用KMP的话时间复杂度也就是原KMP复杂度的n倍,也就是O(NM).那么有什么更加快捷的方式吗,当然

我们可以从暴力来进行一步步优化

如果我们用暴力来求解这道题,显然时间复杂度达到了平方级别一定会爆,那么哪里是可以优化的?

对啦,就在每一个子串之间,我们如果按照KMP的算法来求解,是只把每一个子串与母串的匹配达到了最优,每个子串间是独立的,但是实际上每个子串都可以相互关联,通过利用已求解关联的相关联的子串从而减少当前子串的匹配次数,相互利用,这时候我们就可以用一个结构把他们联系在一起,什么?就是Trie图


建立 Trie图

AC自动机第 1 步

  建立 Trie图也就是字典树,
  跟平常建立是一样滴
void insert(char a[]){
    int p=0,len=strlen(a);
    
    for(int i=0;i<len;i++){
        if(son[p][a[i]-'a']==0) son[p][a[i]-'a']=++cnt;
        p=son[p][a[i]-'a'];
        if(i==len-1) mk[p]++;
    }
                            
}

//son[i][j] 就是 i节点j儿子的编号,注意是++cnt,否则就会有两个编号为0的,但有一个并不是根节点,会出现错误

举个例子
现在有
abcd
b
abd
cd
4个单词,现在我们来建一个树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H22LdI7j-1651314698362)(http
在这里插入图片描述


建立指针

AC自动机第2步

建立指针,也就是常说的fail指针(失配指针)
最最最重要的一步出来啦,那么怎么建立。我们可以观察第一层,它们一定是指向根节点,而接下来的子节点则会指向它们父节点的指向的节点的子节点的编号,也就是fail[son[i][j]]=son[fail[i]][j] (i为父节点也就是当前结点编号,j为子节点的内容比如’a’'b’等)

但是,这样你就会发现一个问题如果按照这样来fail指针是这样的

在这里插入图片描述

az图丑不要在意

蓝色就是正确的,绿色就是错误的,红色就是应该是怎样的

发现了吗,凭肉眼都看出来是abc的后缀和c一样哇,那为什么出现这种问题.因为fail指向的是最长的前缀指针,又因每层不可能长度一样也就导致每层fail指向的都不一样,也就连接了所以与本路径后缀相同的前缀或原本的一个路径,但现在root下的’b’节点缺少’c’这个节点导致了无法连接正确答案,那么可不可以 当然可以不然还说什么 假设’b’有一个’c’节点的儿子,让它储存它应该连接的fail指针的编号,也就是让son[i][j]=son[fail[i]][j],这样我们相当于假设有这个儿子,那么相当于所有的点都有,就可以保证所有点连接,我们让这个假设儿子的编号赋为应连接的那个点编号,相当于成为中转站,可以保证不会中断,出现错误.那么这也意味着每一个搜索时需用到上一层节点,也就意味着是BFS

那么最终建成这样的图
![AC](https://img-blog.csdnimg.cn/img_convert/294540bc65b300853d3454505c801518.png)

int son[500050][30];
int fail[500050];
queue<int>qidx;//存储编号
void get_fail(){
    for(int i=0;i<26;i++){
        fail[son[0][i]]=0;
        if(son[0][i]) qidx.push(son[0][i]);
    }
    int p;
    while(!qidx.empty()){
        p=qidx.front();
        for(int i=0;i<26;i++){
            if(son[p][i]){
                fail[son[p][i]]=son[fail[p]][i];
                qidx.push(son[p][i]);
            }
            else son[p][i]=son[fail[p]][i];
        }
        qidx.pop();
    }
}

匹配母串

  • AC自动机第3步

    匹配母串

    这一步其实也没什么,由于前面已经建立了指针,就意味着只要有一条路可行,其他相关可行的路就被关联算上了,如果没有被算上那就是不行了

int son[500050][30];
int mk[500050];   //标记以这个为节点结束的字符串有几个
int fail[500050];
char ch[1000050];
int m;
int ans;
void AC(){
   int pp=0;
   ans=0;
   for(int i=0;i<m;i++){
       pp=son[pp][ch[i]-'a'];
       for(int j=pp;mk[j]!=-1;j=fail[j]){
           ans+=mk[j];
           mk[j]=-1;
       }
   }
}


#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
int n;
char c[100050];
int cnt;
int son[500050][30];
int mk[500050];
int fail[500050];
char ch[1000050];
int m;
int ans;
queue<int>qidx;//存储编号
void insert(char a[]){
    int p=0,len=strlen(a);
    for(int i=0;i<len;i++){
        if(!son[p][a[i]-'a']) son[p][a[i]-'a']=++cnt;
        p=son[p][a[i]-'a'];
        if(i==len-1) mk[p]++;
    }
}
void get_fail(){
    for(int i=0;i<26;i++){
        fail[son[0][i]]=0;
        if(son[0][i]) qidx.push(son[0][i]);
    }
    int p;
    while(!qidx.empty()){
        p=qidx.front();
        for(int i=0;i<26;i++){
            if(son[p][i]){
                fail[son[p][i]]=son[fail[p]][i];
                qidx.push(son[p][i]);
            }
            else son[p][i]=son[fail[p]][i];
        }
        qidx.pop();
    }
}
void AC(){
    int pp=0;
    ans=0;
    for(int i=0;i<m;i++){
        pp=son[pp][ch[i]-'a'];
        for(int j=pp;mk[j]!=-1;j=fail[j]){
            ans+=mk[j];
            mk[j]=-1;
        }
    }
}
int main(){
    cin>>n;
    for(int i=0;i<n;i++){
        cin>>c;
        insert(c);
    }
    get_fail();
    cin>>ch;
    m=strlen(ch);
    AC();
    cout<<ans<<endl;
    return 0;
}

那么这也就最终完成了QAQ

总而言之就是查找时候把能算的直接算上,没算上的也就是真算不上了

祭洛谷第一篇博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值