AC自动机

P3808 AC 自动机(简单版)

P3796 AC 自动机(简单版 II)

这两题本质上没有什么区别,改变一下统计答案的方式即可。

下面的代码只是板子。

弱化版

#include <bits/stdc++.h> 
#define i8  __int128
// #define int long long 
#define fuck inline
#define lb long double 
using namespace std; 
// typedef long long ll; 
const int N=1e4+5,mod=998244353,S=55,M=1e6+5;
const int INF=1e9+7; 
const int inf=LONG_LONG_MAX/2;
// const int mod1=469762049,mod2=998244353,mod3=1004535809;
// const int G=3,Gi=332748118; 
// const int M=mod1*mod2;
fuck int read()
{
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-'){f=-1;}c=getchar();}
    while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c-'0');c=getchar();}
    return x*f;
}
fuck void write(int x)
{
    if(x<0){putchar('-');x=-x;}
    if(x>9) write(x/10);
    putchar(x%10+'0');
}
int tr[N*S][26],nxt[N*S];
int n,m,idx;
int cnt[N*S];
fuck void insert(string s)//修建字典树
{
    int p=0;s=' '+s;
    for(int i=1;i<s.size();i++)
    {
        int t=s[i]-'a';
        if(!tr[p][t])tr[p][t]=++idx;
        p=tr[p][t];
    }
    cnt[p]++;
}
fuck void build()
{
    queue<int>q;
    for(int i=0;i<26;i++)
    {
        if(tr[0][i])q.push(tr[0][i]);
    }//第一层的信息
    while(!q.empty())
    {
        int t=q.front();q.pop();
        for(int i=0;i<26;i++)//遍历每个节点儿子
        {
            int c=tr[t][i];//儿子的编号
            if(c==0)continue;
            int j=nxt[t];//父亲的失配指针
            while(j&&!tr[j][i])j=nxt[j];
            //如果父亲有失配指针,并且父亲的失配指针下没有对应位置的儿子信息,继续向上层跳失配指针,这里应为是向上层跳,所以前缀的长度会越来越短,直到完成匹配
            if(tr[j][i])j=tr[j][i];//找到了一个符合要求的失配指针
            nxt[c]=j;//更新当前位置的失配指针
            q.push(c);
        }
    }
}
fuck void solve()
{
    memset(tr,0,sizeof(tr));
    memset(nxt,0,sizeof(nxt));
    memset(cnt,0,sizeof(cnt));
    idx=0;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        string s;cin>>s;
        insert(s);
    }
    build();
    string s;cin>>s;s=' '+s;
    int ans=0;
    for(int i=1,j=0;i<s.size();i++)
    {
        int t=s[i]-'a';
        //当前需要匹配的字符,此时的j是某一段字符串的末位,且这个与s[i-1]完成了匹配
        while(j&&!tr[j][t])j=nxt[j];//当前j的位置失配了,往前跳一个合法的位置
        if(tr[j][t])j=tr[j][t];//找到了一个合法的匹配前缀位置,也就是这个位置的前缀在文本串中出现了,并且这个串是最长的
        int p=j;//比他更短的前缀串也必然出现过了,也需要进行统计
        while(p)
        {
            // cout<<p<<endl;
            ans+=cnt[p];//加上以p结尾的贡献
            cnt[p]=0;//清空贡献的价值
            p=nxt[p];//往上跳跃失配指针
        }
    }
    cout<<ans<<"\n";
}
signed main() 
{ 
    // ios::sync_with_stdio(false); 
    // cin.tie(0); cout.tie(0); 
    int QwQ=read();
    // int fuckccf=read();
    while(QwQ--)solve(); 
    // solve(); 
    return 0; 
}
//  6666   66666  666666 
// 6    6  6   6      6 
// 6    6  6666      6 
// 6    6  6  6    6 
//  6666   6   6  6666666

弱化版优化

#include <bits/stdc++.h> 
#define i8  __int128
// #define int long long 
#define fuck inline
#define lb long double 
using namespace std; 
// typedef long long ll; 
const int N=1e4+5,mod=998244353,S=55,M=1e6+5;
const int INF=1e9+7; 
const int inf=LONG_LONG_MAX/2;
// const int mod1=469762049,mod2=998244353,mod3=1004535809;
// const int G=3,Gi=332748118; 
// const int M=mod1*mod2;
fuck int read()
{
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-'){f=-1;}c=getchar();}
    while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c-'0');c=getchar();}
    return x*f;
}
fuck void write(int x)
{
    if(x<0){putchar('-');x=-x;}
    if(x>9) write(x/10);
    putchar(x%10+'0');
}
int tr[N*S][26],nxt[N*S];
int n,m,idx;
int cnt[N*S];
fuck void insert(string s)//修建字典树
{
    int p=0;s=' '+s;
    for(int i=1;i<s.size();i++)
    {
        int t=s[i]-'a';
        if(!tr[p][t])tr[p][t]=++idx;
        p=tr[p][t];
    }
    cnt[p]++;
}
fuck void build()
{
    queue<int>q;
    for(int i=0;i<26;i++)
    {
        if(tr[0][i])q.push(tr[0][i]);
    }//第一层的信息
    while(!q.empty())
    {
        int t=q.front();q.pop();
        for(int i=0;i<26;i++)//遍历每个节点儿子
        {
            int c=tr[t][i];//儿子的编号
            if(c!=0)
            {
                nxt[c]=tr[nxt[t]][i];//如果他爹的失配指针还能继续配对就让他继续配对下去
                //如果他爹的失配指针的儿子里没有他直接会往合法的跳,具体在c==0时
                q.push(c);
            }
            else tr[t][i]=tr[nxt[t]][i];
            //如果他爹没有这个儿子,那就跳到他爹的失配指针下,看看有没有这个儿子
            //如果他爹的失配指针有这个i儿子,那将他的i儿子连过去,相当于在失配时找到了一个更短的前缀去匹配后缀
            //如果他爹的失配指针也没有这个i儿子,那就转移到他爹的失配指针的失配指针有没有这个i儿子
            //由于越跳层数越浅,也就是失配越多次前缀串越短,所以在处理当前节点时,他爹的所有祖先失配指针已经计算好了
            //如果全没有i儿子,就相当于没有前缀串可以匹配当前节点,于是便会跳到0节点,此过程完成了路径的压缩
            //而从c!=0的情况来看,如果说他爹的失配指针一直能找到所需要的儿子,那便会一直传递下去,这样保证了nxt[]里面储存的是当前位置的某一串后缀所能匹配到的最长的前缀
            //所以就算发生了失配,路径压缩后所找到的也是一个以当前结尾的某一个后缀所能匹配的最大前缀
            //妙处在于利用bfs浅层比深层先完成计算的特性(可以认为是无后效性)去直接调用之前的最优答案,从而完成失配后的快速查找
        }
    }
}
fuck void solve()
{
    memset(tr,0,sizeof(tr));
    memset(nxt,0,sizeof(nxt));
    memset(cnt,0,sizeof(cnt));
    idx=0;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        string s;cin>>s;
        insert(s);
    }
    build();
    string s;cin>>s;s=' '+s;
    int ans=0;
    for(int i=1,j=0;i<s.size();i++)
    {
        int t=s[i]-'a';
        //当前需要匹配的字符,此时的j是某一段字符串的末位,且这个与s[i-1]完成了匹配
        j=tr[j][t];//不用判断是否存在tr[j][t]这个儿子,如果不存在会向上找到第一个匹配到的地方
        int p=j;//比他更短的前缀串也必然出现过了,也需要进行统计
        while(p)
        {
            // cout<<p<<endl;
            ans+=cnt[p];//加上以p结尾的贡献
            cnt[p]=0;//清空贡献的价值
            p=nxt[p];//往上跳跃失配指针
        }
    }
    cout<<ans<<"\n";
}
signed main() 
{ 
    // ios::sync_with_stdio(false); 
    // cin.tie(0); cout.tie(0); 
    int QwQ=read();
    // int fuckccf=read();
    while(QwQ--)solve(); 
    // solve(); 
    return 0; 
}
//  6666   66666  666666 
// 6    6  6   6      6 
// 6    6  6666      6 
// 6    6  6  6    6 
//  6666   6   6  6666666

再加一个小优化(面对统计出现串的编号总个数):

while(p&&!vis[p])
{
    ans+=cnt[p];
    cnt[p]=0,vis[p]=1;//如果之前跳过就不要再跳了
    p=nxt[p];
}

洛谷全新正版AC自动机

空降

故事是这样的。。。

与之前的青春版AC自动机不同的地方主要有两个:

  1. 这里的模式串可重复;
  2. 模式串可能很长,导致树的深度很深,每次往上跳处理答案的时间复杂度为 $ O $ (模式串长度),总时间复杂度就为 $ O $ (文本串长度 $ \times $ 模式串长度), $ T $飞。

考虑如何解决这两个问题。

1. 模式串重复问题

这个问题很容易处理,对于一个 $ Tire $ 树上的节点 $ x $ ,定义两个数组 $ cnt $ 和 $ pos $ ,如果编号为 $ i $ 的模式串在节点 $ x $ 处结尾,则令 $ pos_{i}=x $ ,而 $ cnt $ 统计节点 $ x $ 的出现次数,最终访问 $ cnt_{pos_{i}} $ 即可。

2. 对答案统计进行优化

可以发现之前的代码之所以跑得慢是因为我们每次遍历到一个节点就往上更新答案,非常低效。

主播主播,那有没有更高效更鸡贼机智的做法呢?有的,兄弟有的。我们可以发现如果我们处理出每个节点会被计算多少次,那么我们只需要最后维护一次全图的跳跃更新计算即可,所以我们考虑对于一个节点 $ x $ ,让 $ x $ 向 $ nxt_{x} $ 连一条边,表示 $ x $ 将会往 $ nxt_{x} $ 跳,因为一个 $ x $ 只能有一个 $ nxt_{x} $ ,所以最后会形成一棵树,对这棵树从叶子向根更新即可。在连有向边的情况下,我们又可以发现叶子节点的入度必然为 $ 0 $ ,所以最后进行一次拓扑排序统计答案。

code

code
#include <bits/stdc++.h> 
#define i8  __int128
// #define int long long 
#define fuck inline
#define lb long double 
using namespace std; 
// typedef long long ll; 
const int N=2e5+5,mod=998244353,S=70,M=2e6+5;
const int INF=1e9+7; 
// const int inf=LONG_LONG_MAX/2;
// const int mod1=469762049,mod2=998244353,mod3=1004535809;
// const int G=3,Gi=332748118; 
// const int M=mod1*mod2;
fuck int read()
{
    int x=0,f=1;
    char c=getchar();
    while(c<'0'||c>'9'){if(c=='-'){f=-1;}c=getchar();}
    while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c-'0');c=getchar();}
    return x*f;
}
fuck void write(int x)
{
    if(x<0){putchar('-');x=-x;}
    if(x>9) write(x/10);
    putchar(x%10+'0');
}
int tr[N][30];
int cnt[N],pos[N];
int nxt[N];
int idx=0;
string s[N];
fuck void insert(string s,int x)
{
    int p=0;
    for(int i=0;i<s.size();i++)
    {
        int frz=s[i]-'a';
        if(!tr[p][frz])tr[p][frz]=++idx;
        p=tr[p][frz];
    }
    pos[x]=p;
}
int n,ind[N];
vector<int>g[N];
fuck void build()
{
    queue<int>q;
    for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(int i=0;i<26;i++)
        {
            int p=tr[u][i];
            if(p!=0)
            {
                nxt[p]=tr[nxt[u]][i];
                ind[nxt[p]]++;
                g[p].push_back(nxt[p]);
                q.push(p);
            }
            else tr[u][i]=tr[nxt[u]][i];
        }
    }
}
fuck void solve()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>s[i];
        insert(s[i],i);
    }
    build();
    string txt;cin>>txt;
    for(int i=0,j=0;i<txt.size();i++)
    {
        int t=txt[i]-'a';
        j=tr[j][t];
        cnt[j]++;
    }
    queue<int>q;
    for(int i=1;i<=idx;i++)if(!ind[i])q.push(i);
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(auto v:g[u])
        {
            cnt[v]+=cnt[u];
            ind[v]--;
            if(ind[v]==0)q.push(v);
        }
    }
    for(int i=1;i<=n;i++)cout<<cnt[pos[i]]<<"\n";
}
signed main() 
{ 
    // ios::sync_with_stdio(false); 
    // cin.tie(0); cout.tie(0); 
    // int QwQ=read();
    // int fuckccf=read();
    // while(QwQ--)solve(); 
    solve(); 
    return 0; 
}
//  6666   66666  666666 
// 6    6  6   6      6 
// 6    6  6666      6 
// 6    6  6  6    6 
//  6666   6   6  6666666

完结收工!!!!!

个人主页

看完点赞,养成习惯

\(\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\Downarrow\)

posted @ 2025-09-05 21:39  Nightmares_oi  阅读(15)  评论(0)    收藏  举报