AC自动机

模版题:

洛谷3808【模板】AC 自动机(简单版) 裸的AC自动机

洛谷3796【模板】AC 自动机(加强版) 同上,只需要修改统计答案的部分

洛谷5357【模板】AC 自动机(二次加强版) 需要拓扑排序优化

参考博客:

https://oi-wiki.org/string/ac-automaton/

https://www.cnblogs.com/cjyyb/p/7196308.html

在学AC自动机之前,先学好Trie和kmp。

AC自动机用于解决多模式串匹配的问题,建立在Trie的基础上。

先考虑在Trie树上暴力进行多模式串匹配:对于文本串的每一个字符,都在Trie树上从头开始寻找一次,复杂度是O(文本串长度*Trie树深度)。

现在换个角度思考,对于文本串的每一个字符,不考虑以它开头开始遍历,而是考虑以它结尾的模式串有哪些。首先找到一个以它结尾的最长的Trie树上从根开始的链(即找到从根开始的最长的链满足它是文本串当前前缀的后缀),接着直接跳转到另一条次长的链满足上述条件。跳转至的位置就是fail指针(失配指针)所指的节点。

如何求fail?

根(节点0)的fail=0,根的所有子节点的fail=0。

对于其他的节点,它的子节点的fail是它的fail的对应子节点。具体来说:对于任意一个节点u,若trie[u].to[i]存在,则trie[u].to[i]的fail为trie[trie[u].fail].to[i],即节点的fail指针指向的节点的相应子节点;若trie[u].to[i]不存在,则给trie[u].to[i]赋trie[trie[u].fail].to[i](和上面一样)。

bfs按照上述式子求即可。

拓扑排序优化AC自动机:

  • problem:
    在之前的AC自动机中,每次匹配时,对于每一个位置,都需要一直跳fail直到根,这一步很费时
  • hint:
    由于每一个节点都只有一个fail指针,而且一定指向的是比自己深度浅的节点,所以只保留fail指针的话,会形成一棵树,换言之,无环。
    这样一来,每次跳fail相当于在树上往上移动一个点->AC自动机的匹配转换成在fail树上的链求和问题。对于当前的点,需要对fail树上它的祖先节点都贡献一个答案。
  • solution:
    我们并不需要真的建立一棵fail树,只需要记录每个节点的入度(用于拓扑排序)
    在匹配时,先将答案累积到当前节点,然后进行拓扑排序即可

关于end的Tips:

trie树的end可以根据需要赋值,如出现的次数,出现的字符串的编号,当前的节点编号等等
在洛谷模版简单版中,记录的是该串出现的次数,即有多少个模版串是这个串;在洛谷模板加强版中,记录的是串的编号;在洛谷模板二次加强版中,记录的Trie树结尾节点的编号(在这题中其实可以不记录end)。
具体赋值的选择,是为最后统计答案服务的。

另外,注意看清题目说明,模式串是否互不相同。

洛谷3808【模板】AC 自动机(简单版)

//Luogu3808 【模板】AC自动机 https://www.luogu.com.cn/problem/P3808
//给定若干个模式串和一个文本串,求有多少种模式串在文本串中出现过,模式串不同当且仅当它们的编号不同
//By DTTTTTTT
//2023/10/25

#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
const int N=1e6+5;
int n,tot,ans;
string s[N],t;
struct node{
    int to[30];
    int end;
    int fail;
}trie[N];

//构建trie树
void build_trie(){
    for(int j=1;j<=n;++j){ //将s[j]插入trie树
        int cur=0;//根节点编号为0
        int len_j=s[j].length();
        for(int i=0;i<len_j;++i){
            int c=s[j][i]-'a';
            if(!trie[cur].to[c]) trie[cur].to[c]=++tot;
            cur=trie[cur].to[c];
        }
        ++trie[cur].end;
    }
}

//求失配指针
void build_fail(){
    queue<int>q;
    //单独处理根结点的孩子的fail
    for(int i=0;i<26;++i){
        if(trie[0].to[i]){
            trie[trie[0].to[i]].fail=0; //指向根结点
            q.push(trie[0].to[i]);
        }
    }
    //bfs求出所有的fail【失配指针】
    /*
        对于任意一个节点u,若trie[u].to[i]存在,则trie[u].to[i]的fail为trie[trie[u].fail].to[i],
        即节点的fail指针指向的节点的相应子节点。
        若trie[u].to[i]不存在,则给trie[u].to[i]赋trie[trie[u].fail].to[i](和上面一样)。
    */
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=0;i<26;++i){ //枚举所有子节点
            if(trie[u].to[i]){
                trie[trie[u].to[i]].fail=trie[trie[u].fail].to[i];
                q.push(trie[u].to[i]);
            }
            else
                trie[u].to[i]=trie[trie[u].fail].to[i];
        }
    }
}

//匹配求解答案
void ac_solve(){
    int len_t=t.length();
    int cur=0;
    for(int i=0;i<len_t;++i){
        int c=t[i]-'a';
        cur=trie[cur].to[c];
        for(int t=cur;t && trie[t].end!=-1;t=trie[t].fail){
            ans+=trie[t].end;
            trie[t].end=-1; //标记计算过了
        }
    }

}

int main(){
    ios_base::sync_with_stdio(false),cin.tie(0),cout.tie(0);

    //input
    cin>>n;
    for(int i=1;i<=n;++i) cin>>s[i];
    cin>>t;

    //build trie
    build_trie();
    //build fail
    build_fail();

    //求解答案:文本串中出现了多少个不同的模式串
    //注意这里求的是出现的不同模式串的种类数,不是出现次数,即同一个模式串最多对答案贡献一次
    ac_solve();

    cout<<ans<<endl;
    return 0;
}

洛谷3796【模板】AC 自动机(加强版)

//Luogu3796 【模板】AC 自动机(加强版)https://www.luogu.com.cn/problem/P3796
//给定若干个模式串和文本串,输出在文本串中出现次数最多的模式串,按输入顺序排列(保证没有相同的模式串)
//By DTTTTTTT
//2023/10/25

#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
const int N=1e6+5;
int n,tot;
string s[N],t;
struct node{
    int to[30];
    int fail,end;
    //trie树的end可以根据需要赋值,如出现的次数,出现的字符串的编号,当前的节点编号等等
    //在洛谷模版简单版中,记录的是该串出现的次数,即有多少个模版串是这个串
    //在此题(洛谷模板加强版)中,记录的是串的编号
    //具体赋值的选择,是为最后统计答案服务的
}ac[N];
struct node1{
    int id;
    int num;
}ans[N];
void clean(int x){ //初始化ac[x]
    for(int i=0;i<26;++i)
        ac[x].to[i]=0;
    ac[x].fail=ac[x].end=0;
}
void build_trie(){
    for(int j=1;j<=n;++j){
        int len_j=s[j].length();
        int cur=0;
        for(int i=0;i<len_j;++i){
            int c=s[j][i]-'a';
            if(!ac[cur].to[c]) ac[cur].to[c]=++tot,clean(tot);
            cur=ac[cur].to[c];
        }
        ac[cur].end=j; //记录这个字符串的id
    }
}
void build_fail(){
    queue<int> q;
    ac[0].fail=0;
    for(int i=0;i<26;++i){
        int son=ac[0].to[i];
        if(son){
            ac[son].fail=0;
            q.push(son);
        }
    }

    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=0;i<26;++i){
            if(ac[u].to[i]){
                ac[ac[u].to[i]].fail=ac[ac[u].fail].to[i];
                q.push(ac[u].to[i]);
            }
            else   
                ac[u].to[i]=ac[ac[u].fail].to[i];
        }
    }
}

void ac_solve(){
    int cur=0;
    int len_t=t.length();
    for(int i=0;i<len_t;++i){
        int c=t[i]-'a';
        cur=ac[cur].to[c];
        for(int t=cur;t;t=ac[t].fail)
            if(ac[t].end)
                ++ans[ac[t].end].num;
    }
}

bool cmp(node1 x,node1 y){
    if(x.num==y.num) return x.id<y.id;
    return x.num>y.num;
}

int main(){
    ios_base::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    while(1){
        cin>>n;
        if(n==0) break;

        tot=0;
        clean(0);

        for(int i=1;i<=n;++i) {
            cin>>s[i];
            ans[i].id=i;
            ans[i].num=0;
        }
        cin>>t;

        build_trie();
        build_fail();

        ac_solve();

        sort(ans+1,ans+n+1,cmp);

        cout<<ans[1].num<<endl<<s[ans[1].id]<<endl;
        for(int i=2;i<=n;++i)
            if(ans[i].num==ans[1].num)
                cout<<s[ans[i].id]<<endl;
            else    
                break;

    }

    return 0;
}

/*
此题有多组输入,需要每次clear
这里每次对需要使用的节点才clear(学到了!)
具体来说:
    在每组测试例子的开头,设置tot=0,并且clean(0)
    在构建trie树的过程中,若ac[cur].to[c]没有,需要对其赋值++tot时,clean(tot)
*/

洛谷5357【模板】AC 自动机(二次加强版)

//Luogu5357 【模板】AC 自动机(二次加强版)https://www.luogu.com.cn/problem/P5357
//给定若干个模式串和文本串,求出每个模式串在文本串中出现的次数(数据不保证任意两个模式串不相同)
//n <= 2*10^5 模式串的长度总和不超过2*10^5 文本串的长度不超过2*10^6

//【拓扑排序优化AC自动机】
/*
思想:
problem:
    在之前的AC自动机中,每次匹配时,对于每一个位置,都需要一直跳fail直到根,这一步很费时
hint:  
    由于每一个节点都只有一个fail指针,而且一定指向的是比自己深度浅的节点,所以只保留fail指针的话,会形成一棵树,换言之,无环。
    这样一来,每次跳fail相当于在树上往上移动一个点->AC自动机的匹配转换成在fail树上的链求和问题
    对于当前的点,需要对fail树上它的祖先节点都贡献一个答案
solution:
    我们并不需要真的建立一棵fail树,只需要记录每个节点的入度
    在匹配是,先将答案累积到当前节点
    然后进行拓扑排序即可
*/

//By DTTTTTTT
//2023/10/25

#include<iostream>
#include<queue>
using namespace std;
const int N=2e5+5;
int n,tot,in_deg[N],pos[N];
string s[N],t;
struct node{
    int to[30];
    int end,fail;
    int ans;
}ac[N];
void build_trie(){
    for(int j=1;j<=n;++j){
        int len_j=s[j].length();
        int cur=0;
        for(int i=0;i<len_j;++i){
            int c=s[j][i]-'a';
            if(!ac[cur].to[c]) ac[cur].to[c]=++tot;
            cur=ac[cur].to[c];
        }
        ac[cur].end=cur; //其实在这一题中甚至不需要记录end
        pos[j]=cur; //记录s[j]的最后一个字符在trie树上的节点
        /*
        注意这么写是错的:
        ac[cur].end=tot;
        pos[j]=tot; //记录s[j]的最后一个字符在trie树上的tot节点
        因为在重复出现同一个模式串的前提下,tot并不代表当前结尾的节点编号
        */
    }
}
void build_fail(){
    queue<int>q;
    ac[0].fail=0;
    for(int i=0;i<26;++i)
        if(ac[0].to[i]){
            ac[ac[0].to[i]].fail=0;
            q.push(ac[0].to[i]);
        }
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=0;i<26;++i){
            if(ac[u].to[i]) {
                ac[ac[u].to[i]].fail=ac[ac[u].fail].to[i];
                ++in_deg[ac[ac[u].fail].to[i]]; //增加入度
                q.push(ac[u].to[i]);
            }
            else ac[u].to[i]=ac[ac[u].fail].to[i];
        }
    }
}
void ac_solve(){
    int len_t=t.length();
    int cur=0;
    for(int i=0;i<len_t;++i){
        int c=t[i]-'a';
        cur=ac[cur].to[c];
        ++ac[cur].ans;
    }
}
void topu(){
    queue<int>q;
    for(int i=1;i<=tot;++i)
        if(!in_deg[i]) q.push(i);
    
    while(!q.empty()){
        int u=q.front();
        q.pop();
        int fa=ac[u].fail;
        --in_deg[fa];
        ac[fa].ans+=ac[u].ans;
        if(in_deg[fa]==0) q.push(fa);
    }
}
int main(){
    ios_base::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;++i) cin>>s[i];
    cin>>t;

    build_trie();
    build_fail();

    ac_solve();
    topu();

    for(int i=1;i<=n;++i)
        cout<<ac[pos[i]].ans<<endl;
    
    return 0;
}
posted @   DTTTTTTT-  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示