AC自动机

AC自动机讲解

void ins(char *s, int id)
{
    int p=0;
    for(re i=0, len=strlen(s);i<len;++i)
    {
        int x=s[i]-'a';
        if(!go[p][x])go[p][x]=++tot;
        p=go[p][x];
    }
    //操作一下 
}
void build()
{
    for(re i=0;i<26;++i)
        if(go[0][i])
            Q.push(go[0][i]);
    while(!Q.empty())
    {
        int x=Q.front();
        Q.pop();
        for(re i=0;i<26;++i)
        {
            if(go[x][i])
            {
                int t=go[x][i];
                fail[t] = go[fail[x]][i];
                //操作一下 
                Q.push(t);
            }
            else go[x][i] = go[fail[x]][i];
        }
    }
}
void query(char *s)
{
    int p=0;
    for(re i=0, len=strlen(s);i<len;++i)
    {
        int x=s[i]-'a';
        p=go[p][x];
        cnt[p]++;
    }
}

例题:

例1:AC自动机(二次加强版)

简要题意:给你一个文本串Sn个模式串,请你分别求出每个模式串在S中出现的次数。

分析:

这道题我们可不能直接暴力的用AC自动机,去匹配每一个模式串,这会TLE。

我们用一种很优秀的方法,拓扑排序!来解决这道题。

我们想象一下,模式串a与aa出现的次数,肯定a要比aa多。

同时,a可以为我们AC自动机中,aa的fail。

所以,我们用拓扑排序,从字典树下端推到上端,这样就可以了。

#include<bits/stdc++.h>
using namespace std;
#define re register int
const int N=2e6+5;
int go[N][26], fail[N], cnt[N], tot, bel[N], in[N];
queue<int>Q;
void ins(char *s, int id)
{ // 加入模式串 
    int p=0;
    for(re i=0, len=strlen(s);i<len;++i)
    {
        int x=s[i]-'a';
        if(!go[p][x])go[p][x]=++tot;
        p=go[p][x];
    }
    bel[id] = p; 
// 标记第id个模式串的末尾在哪里,末尾的cnt个数,即为模式串出现的个数 
}
void build()
{ // 构建fail数组 
    for(re i=0;i<26;++i)
        if(go[0][i])
            Q.push(go[0][i]);
    while(!Q.empty())
    {
        int x=Q.front();
        Q.pop();
        for(re i=0;i<26;++i)
        {
            if(go[x][i])
            {
                int t=go[x][i];
                fail[t] = go[fail[x]][i];
                in[fail[t]]++; // 处理拓扑排序 
                Q.push(t);
            }
            else go[x][i] = go[fail[x]][i];
        }
    }
}
void solve()
{ // topsort
    for(re i=0;i<=tot;++i)
        if(in[i] == 0)
            Q.push(i);
    while(!Q.empty())
    {
        int x=Q.front();
        Q.pop();
        
        int v=fail[x];
        in[v]--;
        cnt[v]+=cnt[x];
        if(in[v]==0)Q.push(v);
    }
}
void query(char *s)
{
    int p=0;
    for(re i=0, len=strlen(s);i<len;++i)
    {
        int x=s[i]-'a';
        p=go[p][x];
        cnt[p]++;
    }
}
char s[5000000];
int main()
{
    int n;
    scanf("%d",&n);
    for(re i=1;i<=n;++i)
    {
        scanf("%s",s);
        ins(s, i);
    }
    build();
    scanf("%s",s);
    query(s);
    solve();
    for(re i=1;i<=n;++i)printf("%d\n", cnt[bel[i]]);
}
View Code

例2:电脑游戏

简要题意:你有n中可以得到贡献1的串,已知主串长度为k,求最大能得到的贡献。

例如:三个串"ABA","CB",和"ABACB",主串长度为5,若主串为"ABACB",则得到三分。

分析:

这道题很明显是在AC自动机上的dp。

我们定状态 dp[i][p],表示现在主串讨论到第i位,在AC自动机上的位置为p。

ps:这个“在AC自动机上的位置p”,其实是:最小的能用来表示主串长什么样子的值。

for(re i=0;i<m;++i)
    for(re j=0;j<=tot;++j)
        for(re k=0;k<3;++k)
        {
            int ch=go[j][k];
            f[i+1][ch]=max(f[i+1][ch],f[i][j]+val[ch]);
        }
#include<bits/stdc++.h>
using namespace std;
#define re register int
int go[5005][3], val[5005], fail[5005], tot;
inline void insert(string s)
{
    int p=0;
    for(re i=0,len=s.length();i<len;++i)
    {
        int x=s[i]-'A';
        if(!go[p][x])go[p][x]=++tot;
        p=go[p][x];
    }
    val[p]=1;
}
inline void build()
{
    queue<int>Q;
    for(re i=0;i<3;++i)if(go[0][i])Q.push(go[0][i]);
    while(!Q.empty())
    {
        int x=Q.front();Q.pop();
        for(re i=0;i<3;++i)
        if(go[x][i])
        {
            int t=go[x][i];
            Q.push(t);
            fail[t]=go[fail[x]][i];
            val[t]+=val[fail[t]];
        }
        else go[x][i]=go[fail[x]][i];
    }
}
int f[1005][5005];
signed main()
{
    int n, m;
    cin>>n>>m;
    while(n--)
    {
        string s;cin>>s;
        insert(s);
    }
    build();
    
    memset(f,-0x3f,sizeof(f));
    f[0][0]=0;int ans=0;
    for(re i=0;i<m;++i)
    {
        for(re j=0;j<=tot;++j)
        {
            for(re k=0;k<3;++k)
            {
                int ch=go[j][k];
                f[i+1][ch]=max(f[i+1][ch],f[i][j]+val[ch]);
            }
        }
    }
    for(re i=0;i<=tot;++i)ans=max(ans,f[m][i]);
    printf("%d",ans);
}
View Code

例3:DNA修复

简要题意:DNA是一个用“ACGT”构成的字符串,其中有一些子串是不可以出现的,我们需要修改一些字母来满足要求。问最少需要修改的次数。

分析:这也是dp。

dp[i][p]表示讨论到主串第i位,当前主串在AC自动机的p位,满足要求所需的最小代价。

#include<bits/stdc++.h>
using namespace std;
#define re register int
int go[1005][4], val[1005], fail[1005], tot;
inline int gt(char x)
{
    if(x=='A')return 0;
    if(x=='T')return 1;
    if(x=='G')return 2;
    return 3;
}
inline void insert(string s)
{
    int p=0;
    for(re i=0,len=s.length();i<len;++i)
    {
        int x=gt(s[i]);
        if(!go[p][x])go[p][x]=++tot;
        p=go[p][x];
    }
    val[p]=1;
}
inline void build()
{
    queue<int>Q;
    for(re i=0;i<4;++i)if(go[0][i])Q.push(go[0][i]);
    while(!Q.empty())
    {
        int x=Q.front();Q.pop();
        for(re i=0;i<4;++i)
        if(go[x][i])
        {
            int t=go[x][i];
            Q.push(t);
            fail[t]=go[fail[x]][i];
            val[t]|=val[fail[t]];
        }
        else go[x][i]=go[fail[x]][i];
    }
}
int f[1005][1005];
signed main()
{
    int n;cin>>n;
    while(n--)
    {
        string s;cin>>s;
        insert(s);
    }
    build();
    string S;cin>>S;
    
    memset(f,0x3f,sizeof(f));f[0][0]=0;
    
    int ans=1e9,m=S.length();
    for(re i=0;i<m;++i)
    {
        for(re j=0;j<=tot;++j)
        {
            for(re k=0;k<4;++k)
            {
                int p=go[j][k];
                if(!val[p])f[i+1][p]=min(f[i+1][p],f[i][j]+(gt(S[i])!=k));
            }
        }
    }
    for(re i=0;i<=tot;++i)ans=min(ans,f[m][i]);
    printf("%d",(ans==1000000000?-1:ans));
}
View Code

例4:【SDOI2014 R1D1】数数

简要题意:

我们称一个正整数N是幸运数,当且仅当它的十进制表示中不包含数字串集合S中任意一个元素作为子串。例如当S={22,333,0233}时,233是幸运数,2333、20233、3223都不是幸运数。给定N和S,计算不大于N的幸运数个数。

分析:

这道题是 数位dp+AC自动机。kzsn有点说不清楚,以后应该会补。

#include<bits/stdc++.h>
using namespace std;
#define re register int
const int mo=1e9+7;
int go[1505][20], jud[1505], fail[1505], f[1505][1505][15], T[1505], tot;
inline void insert(char *s)
{
    int p=0;
    for(re i=0,len=strlen(s);i<len;++i)
    {
        int x=s[i]-'0';
        if(!go[p][x])go[p][x]=++tot;
        p=go[p][x];
    }
    jud[p]=1;
}
inline void build()
{
    queue<int>Q;
    for(re i=0;i<10;++i)if(go[0][i])Q.push(go[0][i]);
    while(!Q.empty())
    {
        int x=Q.front();Q.pop();
        jud[x] |= jud[fail[x]];
        for(re i=0;i<10;++i)
        {
            int t=go[x][i];
            if(t)
            {
                Q.push(t);
                fail[t] = go[fail[x]][i];
            }
            else go[x][i]=go[fail[x]][i];
        }
    }
    go[0][0]=0;
}
char ch[1505];
signed main()
{
    scanf("%s",ch+1);
    int len=strlen(ch+1), m;scanf("%d",&m);
    for(re i=1;i<=len;++i)T[i]=(int)(ch[i]-'0');
    
    while(m--){scanf("%s",ch);insert(ch);}
    build();
    
    f[0][0][1]=1;
    
    for(re i=0;i<len;++i)
    for(re j=0;j<=tot;++j)
    for(re l=0;l<=1;++l)
    if(f[i][j][l])
    for(re k=(l==1?T[i+1]:9);k>=0;--k)
    if(!jud[go[j][k]])
    {
        int t=go[j][k];
        f[i+1][t][(l&(T[i+1]==k))] = (f[i+1][t][(l&(T[i+1]==k))] + f[i][j][l])%mo;
    }
    
    int ans = mo-1;
    for(re i=0;i<=tot;++i)ans = (ans + (f[len][i][0] + f[len][i][1]) % mo) % mo;
    printf("%d",ans);
}
View Code

总结:

AC自动机,是一个可以用来匹配主串与模式串的优秀算法,常常可以用来帮助dp。

其中的fail数组也很是神奇,与kmp中的fail数组有异曲同工之妙。

kzsn还需多加练习。

这道题已补

 

 

 

posted @ 2021-07-30 21:27  kzsn  阅读(107)  评论(0编辑  收藏  举报