# Ac自动机

Ac自动机

Ac自动机=Trie+Kmp

Kmp处理的是某个单词在文章中是否出现,出现次数,出现位置等问题,Ac自动机处理的是一堆单词在文章中是否出现,出现次数,出现位置等问题。

Ac自动机通过将一堆单词存储在Trie树中,并且对树上的每个节点建立失配指针ne[i](类似KMP对单词的每个字母建立失配指针),再将文章放在Trie树上跑,实现字符串的匹配问题。

失配指针如下图所示,红色表示失配指针指向。具体实现OI-wiki中的gif动图

模板题1:Acwing 1282 搜索关键词

给定n个单词,和一篇文章,问有多少个单词在文章中出现了。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=10010,S=55,M=1000010;

int n;
int tr[N*S][26],cnt[N*S],idx;
char str[M];
int q[N*S],ne[N*S];
inline void insert(){//插入过程和Trie的插入过程一模一样
    int p=0;
    for(int i=0;str[i];i++){
        int t=str[i]-'a';
        if(!tr[p][t])tr[p][t]=++idx;
        p=tr[p][t];
    }
    cnt[p]++;
}

inline void build(){//构造树上失配指针ne[]
    int hh=0,tt=-1;
    for(int i=0;i<26;i++)
        if(tr[0][i])
            q[++tt]=tr[0][i];

    //bfs,一层一层处理
    while(hh<=tt){
        int t=q[hh++];
        for(int i=0;i<26;i++){
            int p=tr[t][i];//将当前节点取出来
            if(!p)tr[t][i]=tr[ne[t]][i];//如果当前节点为空,将不存在的字典树的状态等价到已经存在的另一状态,类比失配的状态
            else {
                ne[p]=tr[ne[t]][i];//如果trie中有这个节点,那么这个节点p的fail指针指向p的父节点t的fail指针指向的位置,父节点t的fail指针在p之前已经预处理过了,相当于动态规划。
                q[++tt]=p;
            }
        }
    }
}
int main(){
    int t;cin>>t;
    while (t--){
        memset(tr,0,sizeof(tr));
        memset(cnt,0,sizeof(cnt));
        memset(ne,0,sizeof(ne));
        idx=0;

        cin>>n;
        for(int i=0;i<n;i++){
            cin>>str;
         //   cout<<str<<endl;
            insert();
        }

        build();

        cin>>str;//读入文章
      //  cout<<str<<endl;
        int res=0;

        //类似KMP的匹配过程,只不过这个过程变成了在树上实现
        for(int i=0,j=0;str[i];i++){
            int t=str[i]-'a';
            j=tr[j][t];
            int p=j;
            while (p){
                res+=cnt[p];
                cnt[p]=0;//只需要判断是否出现过,不需要统计出现过多少次,所以当知道出现过的时候,就可以置0了
                p=ne[p];
            }
        }
        cout<<res<<endl;
    }
    return 0;
}

模板题2:ACwing 1285 单词

给出n个单词,问每个单词在这些单词中出现多少次。

思路:对给出的n个单词建立Trie树,单词每个字母走过的节点,该节点都要加一。最后统计的时候,从叶节点开始往上累加,后缀出现的次数累加到KMP对应前缀出现的次数上。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N =1e6+5;
int n;
int tr[N][26],f[N],idx;//f[]记录每个节点存了多少个相同的数
int q[N],ne[N];
char str[N];
int id[210];//记录每个单词的结束位置

inline void insert(int x){
    int p=0;
    for(int i=0;str[i];i++){
        int t=str[i]-'a';
        if(!tr[p][t])tr[p][t]=++idx;
        p=tr[p][t];
        f[p]++;//单词的每个字母在trie上出现的地方都要加1
    }
    id[x]=p;
}
void build(){
    int hh=0,tt=-1;
    for(int i=0;i<26;i++)
        if(tr[0][i])q[++tt]=tr[0][i];

    while(hh<=tt){
        int t=q[hh++];
        for(int i=0;i<26;++i){
            int& p=tr[t][i];
            if(!p)p=tr[ne[t]][i];
            else {
                ne[p]=tr[ne[t]][i];
                q[++tt]=p;
            }
        }
    }
}
int main(){
    cin>>n;
    for(int i=0;i<n;i++){
        cin>>str;
        insert(i);
    }
    build();

    //队列中存储的顺序恰好是树的层次遍历顺序,相应后缀出现的次数累加到KMP对应前缀出现的次数
    for(int i=idx-1;i>=0;i--)f[ ne[ q[i] ] ]+=f[ q[i] ];

    for(int i=0;i<n;i++)cout<<f[id[i]]<<endl;
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/sstealer/p/12273034.html
今日推荐