AC自动机


最近真是太颓了,做了一堆板子题,现在对一些知识点顺便来个总结记录

原理

大家应该都知道KMP和Trie树吧,不懂的可以看我博客或到网上自己动手寻找资料。AC自动机是一个很好的东西,这是因为它的名字很好它能够在有多个模式串的时候进行全文匹配,这十分方便地扩展了KMP的功能,实际上它的思路与原理与KMP十分地类似

首先,和KMP一样,我们要把根节点和根的儿子节点们的 Fail 函数都赋值为0,然后,我们考虑在Trie树上进行BFS,同时像KMP一样,不断地向前推,同时还要处理一个 Last 数组,用于保存每个节点沿失配边走所遇到的第一个单词节点,因为我们在找到一个模式串的时候,也许还会有许多其他的模式串也被匹配了,所以我们要不断地再沿着 Last 走即可

代码

#include<queue>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#define maxn 105
#define maxn1 1000005
#define maxn2 100005
#define sigma_size 26
using namespace std;
char txt[maxn1],tmp[maxn2];
int len1,n,size=1,f[maxn1],last[maxn1],ans=0;
bool vis[maxn];

//struct Trie{
    int ch[maxn1][sigma_size];
    int val[maxn1];
    int id;
    void Trie(){size=1;memset(ch[0],0,sizeof(ch[0]));}
    int idx(char c){return c-'a';}

    void insert(char* s,int v){
        int u=0,len=strlen(s);
        for(int i=0;i<len;i++){
            int c=idx(s[i]);
            if(!ch[u][c]){
                memset(ch[size],0,sizeof(ch[size]));
                val[size]=0;
                ch[u][c]=size++;
            }
            u=ch[u][c];
        }
        val[u]=v;
    }
//};

void init(){
    f[0]=0;
    queue<int> bfs;
    for(int i=0;i<sigma_size;i++){
        int u=ch[0][i];
        if(!u)continue;
        f[u]=0;
        bfs.push(u);
        last[u]=0;
    }
    while(!bfs.empty()){
        int r=bfs.front();bfs.pop();
        for(int i=0;i<sigma_size;i++){
            int u=ch[r][i];
            if(!u){
                ch[r][i]=ch[f[r]][i];
                continue;
            }
            bfs.push(u);
            int v=f[r];
            while(v&&!ch[v][i])v=f[v];
            f[u]=ch[v][i];
            last[u]=val[f[u]]?f[u]:last[f[u]];
        }
    }
}
void accumulate(int x){
    if(x){
        vis[val[x]]=1;
        accumulate(last[x]);
    }
    return;
}
void AC_AM(){
    init();
    int j=0;
    for(int i=0;i<len1;i++){
        int u=idx(txt[i]);
        //while(j&&!ch[j][u])j=f[j];
        j=ch[j][u];
        if(val[j])accumulate(j);
        else if(last[j])accumulate(last[j]);
    }
    return;
}
int main(){
    scanf("%s",txt);
    len1=strlen(txt);
    scanf("%d\n",&n);
    Trie();
    for(int i=0;i<n;i++){
        scanf("%s",tmp);
        insert(tmp,i+1);
    }
    AC_AM();
    for(int i=1;i<=n;i++){
        if(vis[i])ans++;
    }
    printf("%d",ans);
    return 0;
}

大概就是这样

细节

  1. 我们可以进行一些小小的优化,比如我们可以把不存在的节点连接回存在的节点,即让它自动沿着 Fail 走,这样其实非常方便

  2. 在AC自动机上,或者说是在Trie树上,我们所谓的失配,就从KMP中的节点不匹配变化为了节点不存在,所以我们每次只要把 Σ (字母表)中的元素都枚举一遍即可

总结

没写的时候感觉很难,写完了以后其实也挺难,但实际上并不难