后缀自动机总结

本文分享了作者学习和实践后缀自动机的经验,详细解析了几道典型题目,包括最长公共子串、子串出现次数统计等,展示了如何通过后缀自动机高效解决问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


后缀自动机总结


很长时间没写过博客了,该写写了。

最近一直在学习和练习后缀自动机,总体的感觉还不错。
后缀自动机只有一个关键函数—-就是建树(但是匹配好难想)。
后缀自动机的每一个节点都代表着一个后缀,所有的后缀在后缀自动机上都可以匹配。
后缀自动机中,fa[]与AC自动机的fail指针作用似乎是相同的。
后缀自动机中,很多题目中都用到了基数排序(以maxlen排序相当于拓扑排序)。
至于算法的学习,自己去看博客吧!传送门

总结一下所写的题目:


SPOJ1811

题意:一个串和另一个串中的最长公共子串。

这道题算是入门题

我对于这道题到是没什么想法(可能是初学),写了个模板却不会匹配。。。
其实,还是比较简单,只需将一个串建自动机,一个串进行匹配即可,失配了就回退,直到可以匹配,并修改len值。否则就++len,当ans<len,时更新答案。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#define For(aa,bb,cc) for(int aa=bb;aa<=cc;++aa)
#define Set(aa,bb) memset(aa,bb,sizeof(aa))
using namespace std;
const int maxn=500010;
int lena,lenb,last;
char a[maxn],b[maxn];
int tot,st[maxn][26],dis[maxn],fa[maxn];

void init(){tot=last=1;}

void add(int pos){
    int x=a[pos]-'a',p=last,np=++tot;
    last=np,dis[np]=pos;
    for(;p && !st[p][x];p=fa[p]) st[p][x]=np;
    if(!p) fa[np]=1;
    else{
        int q=st[p][x];
        if(dis[q]==dis[p]+1) fa[np]=q;
        else{
            int nq=++tot;
            dis[nq]=dis[p]+1;
            For(i,0,25) st[nq][i]=st[q][i];
            fa[nq]=fa[q],fa[np]=fa[q]=nq;
            for(;st[p][x]==q;p=fa[p]) st[p][x]=nq;
        }
    }
}

int main(){
    init();
    scanf("%s%s",a+1,b+1);
    lena=strlen(a+1),lenb=strlen(b+1);
    For(i,1,lena) add(i);
    int ans=0,len=0,p=1;
    For(i,1,lenb){
        int x=b[i]-'a';
        if(st[p][x]) ++len,p=st[p][x];
        else{
            while(p && !st[p][x]) p=fa[p];
            if(!p) p=1,len=0;
            else len=dis[p]+1,p=st[p][x];
        }
        if(ans<len) ans=len;
    }
    printf("%d\n",ans);
}

SPOJ8222

题意:
对于一个字符串S,找出长度从1到len(S)的每一个长度的一个子串出现次数最多的次数。

这是我觉的这是让我印象最深的一道题。

主要思路:
    对于后缀自动机而言,有两个DAG,一个是parent树,一个是trans树,trans树是原串的所有后缀,parent树是原串的反串的所有后缀。
    而题目所需要求的,便是对于每一个子串,能够从这个点到root的路径的方案数,所以有以下两种方法。
    1.从trans树中找到对于的right(S)集合的大小。
    2.从parent树中来解决。(当然我是用第二种)--||。
    先将原树中的后缀先标记出来,a[]=1,但是我们并没有将原树中的所有儿子记录下来,所以可以通过一个神奇的性质--儿子节点的len一定大于父亲节点的len,我们就可以用len值进行从大到小排序,然后,用当前点来更新fa。
    3.节点代表的是一个区间的长度,但是长串这个节点出现了,则短串一定会出现,所以就先只考虑最长的长度,然后再用长串更新短串。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<cstdlib>
#define For(aa,bb,cc) for(int aa=bb;aa<=(int)cc;++aa)
#define Set(aa,bb) memset(aa,bb,sizeof(aa))
#define Forr(aa,bb,cc) for(int aa=bb;aa>=(int)cc;--aa)
using namespace std;
const int maxn=500000+10;
char s[maxn];
int last,tot,len,maxlen[maxn],tree[maxn][26],fa[maxn];
int a[maxn],b[maxn],c[maxn],f[maxn];

inline void init(){ last=tot=1; }

void add_char(int pos){
    int np=++tot,p=last,x=s[pos]-'a';
    last=np;maxlen[np]=pos;
    for(;p && !tree[p][x];p=fa[p]) tree[p][x]=np;
    if(!p) fa[np]=1;
    else{
        int q=tree[p][x];
        if(maxlen[q]==maxlen[p]+1) fa[np]=q;
        else{
            int nq=++tot;
            maxlen[nq]=maxlen[p]+1;
            For(i,0,25) tree[nq][i]=tree[q][i];
            fa[nq]=fa[q];fa[q]=fa[np]=nq;
            for(;tree[p][x]==q;p=fa[p]) tree[p][x]=nq;
        }
    }
}

void sort_s(){
    For(i,1,tot) ++b[maxlen[i]];
    For(i,1,len) b[i]+=b[i-1];
    For(i,1,tot) c[b[maxlen[i]]--]=i;
}

int main(){
#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
#endif
    init();
    scanf("%s",s+1);
    len=strlen(s+1);
    For(i,1,len) add_char(i);
    int p=1;
    For(i,1,len) p=tree[p][s[i]-'a'],++a[p];
    sort_s();
    Forr(i,tot,1) a[fa[c[i]]]+=a[c[i]];
    For(i,1,tot) f[maxlen[i]]=max(f[maxlen[i]],a[i]);
    Forr(i,len,1) f[i]=max(f[i],f[i+1]);
    For(i,1,len) printf("%d\n",f[i]);
    return 0;
}

POJ1509

题意:
    一个字符串,求以它的哪一个位置开始的字典序最小。

这一题比较简单。

1.最小表示法,裸题。不做说明。

2.后缀自动机:

    将字符串复制1次,加到后面,以此时的len建立自动机。它的巧妙之处便是这个。然后贪心的选取长度为原串的子串,此时就可以得到答案。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define For(aa,bb,cc) for(int aa=bb;aa<=(int)cc;++aa)
#define Forr(aa,bb,cc) for(int aa=bb;aa>=(int)cc;--aa)
#define Set(aa,bb) memset(aa,bb,sizeof(aa))
using namespace std;
const int maxn=50010;
int n;
char s[maxn];
int st[maxn][26],fa[maxn],maxlen[maxn],last,tot;

void init(){
    last=tot=1;Set(st,0),Set(maxlen,0),Set(fa,0);
}

void add_char(int pos){
    int x=s[pos]-'a',p=last,np=++tot;
    last=np;maxlen[np]=pos;
    for(;p && !st[p][x];p=fa[p]) st[p][x]=np;
    if(!p) fa[np]=1;
    else{
        int q=st[p][x];
        if(maxlen[q]==maxlen[p]+1) fa[np]=q;
        else{
            int nq=++tot;
            maxlen[nq]=maxlen[p]+1;
            For(i,0,25) st[nq][i]=st[q][i];
            fa[nq]=fa[q],fa[q]=fa[np]=nq;
            for(;st[p][x]==q;p=fa[p]) st[p][x]=nq;
        }
    }
}

int main(){
    int _;
    scanf("%d",&_);
    while(_--){
        init();
        scanf("%s",s+1);
        int len=strlen(s+1);
        For(i,1,len) s[i+len]=s[i];
        For(i,1,2*len) add_char(i);
        int u=1;
        For(i,1,len)
            For(j,0,25) 
                if(st[u][j]){ u=st[u][j];break; }
        printf("%d\n",maxlen[u]-len+1);
    }
    return 0;
}

hdu4436
题意:求一串数字的所有子串和。
感悟:
本题是后缀自动机的一个经典题型,它主要的思路是利用每一个节点与父亲节点的差距就是在后边增加了一个字符。所以,对于本题而言,这个的大小是父亲节点*10+这个节点的值,利用这个性质,可以得到一个巧妙的计算方法。从这个节点开始的儿子的个数,即路径的条数cnt,将父亲的权值*10*cnt加上所有新加的点的权值和。统计所有的,即为答案。

#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#define For(aa,bb,cc) for(int aa=(bb);aa<=(int)(cc);++aa)
#define Set(aa,bb) memset(aa,bb,sizeof(aa))
using namespace std;
const int maxn=4e5+10,mod=2012;
int n,len,last,tot;
int maxlen[maxn],tree[maxn][11],fa[maxn];
char s[maxn];
int sum[maxn],id[maxn];
int road[maxn],tmp[maxn];

inline void init(){ last=tot=1,len=0; }

void add_char(int pos,int x){
    int p=last,np=++tot;last=np;
    maxlen[np]=pos;
    for(;p && !tree[p][x];p=fa[p]) tree[p][x]=np;
    if(!p) fa[np]=1;
    else{
        int q=tree[p][x];
        if(maxlen[q]==maxlen[p]+1) fa[np]=q;
        else{
            int nq=++tot;
            maxlen[nq]=maxlen[p]+1;
            For(i,0,10) tree[nq][i]=tree[q][i];
            fa[nq]=fa[q],fa[q]=fa[np]=nq;
            for(;tree[p][x]==q;p=fa[p]) tree[p][x]=nq;
        }
    }
}

int main(){
    while(~scanf("%d",&n)){
        init();
        while(n--){
            scanf("%s",s+1);
            for(int i=1;s[i];++i) add_char(++len,s[i]-'0');
            add_char(++len,10);
        }
        Set(sum,0);
        For(i,1,tot) ++sum[maxlen[i]];
        For(i,1,len) sum[i]+=sum[i-1];
        For(i,1,tot) id[sum[maxlen[i]]--]=i;
        road[1]=1,tmp[1]=0;
        int ans=0;
        For(i,1,tot){
            int p=id[i],q;
            For(j,0,9){
                if(i==1 && j==0) continue;
                if(tree[p][j]){
                    q=tree[p][j];
                    road[q]=(road[q]+road[p])%mod;
                    tmp[q]=(tmp[q]+tmp[p]*10+road[p]*j)%mod;
                }
            }
            ans=(ans+tmp[p])%mod;
        }
        printf("%d\n",ans);
        For(i,1,tot) road[i]=tmp[i]=maxlen[i]=id[i]=fa[i]=0,Set(tree[i],0);
    }
    return 0;
}

SPOJ1812
题意:n个字符串的LCS。
感悟:
这道题,是我学习基数排序的开始,将每一个节点在的拓扑序给记录了下来,可以通过每个节点的含义,来进行一些操作。对于一个串,建立后缀自动机,然后拿其他的串在上面进行匹配,需要记录当前的串匹配的最长长度和前面所有串匹配的最长长度的最短长度。更新时倒过来,从长度大的更新到长度小的,因为它的后缀出现的次数一定大于等于它的出现次数,并且它的拓扑排序后,深度大的节点,编号大。

#include<cstdio>
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#define For(aa,bb,cc) for(int aa=bb;aa<=(int)cc;++aa)
#define Forr(aa,bb,cc) for(int aa=bb;aa>=(int)cc;--aa)
using namespace std;
const int maxn=2e5+10,inf=0x3f3f3f3f;
int tot,last,len,tree[maxn][26],fa[maxn],maxlen[maxn];
char s[maxn],ss[maxn];
int ans[maxn],tmp[maxn],id[maxn],sum[maxn];

inline void init(){
    tot=last=1;
    For(i,0,25) tree[0][i]=1;
    maxlen[0]=-1;
}

void add_char(int pos){
    int np=++tot,p=last,x=s[pos]-'a';
    maxlen[np]=pos;
    for(;p && !tree[p][x];p=fa[p]) tree[p][x]=np;
    if(!p) fa[np]=1;
    else{
        int q=tree[p][x];
        if(maxlen[q]==maxlen[p]+1) fa[np]=q;
        else{
            int nq=++tot;
            maxlen[nq]=maxlen[p]+1;
            For(i,0,25) tree[nq][i]=tree[q][i];
            fa[nq]=fa[q],fa[q]=fa[np]=nq;
            for(;tree[p][x]==q;p=fa[p]) tree[p][x]=nq;
        }
    }
    last=np;
}

int main(){
#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
#endif
    init();
    scanf("%s",s+1);
    len=strlen(s+1);
    For(i,1,len) add_char(i);
    For(i,1,tot) ++sum[maxlen[i]];
    For(i,1,len) sum[i]+=sum[i-1];
    For(i,1,tot) id[sum[maxlen[i]]--]=i;
    For(i,1,tot) ans[i]=inf;
    while(scanf("%s",ss+1)!=EOF){
        len=strlen(ss+1);
        int p=1,now=0;
        For(i,1,len){
            int x=ss[i]-'a';
            for(;!tree[p][x];p=fa[p]) now=maxlen[fa[p]];
            p=tree[p][x];
            tmp[p]=max(tmp[p],++now);
        }
        Forr(i,tot,1){
            int x=id[i];
            ans[x]=min(ans[x],tmp[x]);
            if(tmp[x]) tmp[fa[x]]=maxlen[fa[x]];
            tmp[x]=0;
        }
        For(i,1,tot) tmp[i]=0;
    }
    int final_ans=0;
    For(i,1,tot) final_ans=max(final_ans,ans[i]);
    printf("%d\n",final_ans);
    return 0;
}

spoj7258
题意:找第k大的子串。
感悟:
主席树的搜索方式。
先预处理出来每个点的儿子的个数,主席树的方式查询即可。
预处理从儿子,递推到root。

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#define For(aa,bb,cc) for(int aa=(bb);aa<=(int)(cc);++aa)
#define Forr(aa,bb,cc) for(int aa=(bb);aa>=(int)(cc);--aa)
using namespace std;
const int maxn=180010;
char s[maxn];
int tree[maxn][26],fa[maxn],maxlen[maxn],last,tot,len;
int id[maxn],sum[maxn],f[maxn];

inline void init(){
    last=tot=1;
}

void add_char(int pos){
    int np=++tot,p=last,x=s[pos]-'a';
    last=np;maxlen[np]=pos;
    for(;p && !tree[p][x];p=fa[p]) tree[p][x]=np;
    if(!p) fa[np]=1;
    else{
        int q=tree[p][x];
        if(maxlen[q]==maxlen[p]+1) fa[np]=q;
        else{
            int nq=++tot;
            maxlen[nq]=maxlen[p]+1;
            For(i,0,25) tree[nq][i]=tree[q][i];
            fa[nq]=fa[q];fa[q]=fa[np]=nq;
            for(;p && tree[p][x]==q;p=fa[p]) tree[p][x]=nq;
        }
    }
}

int main(){
#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
#endif
    init();
    scanf("%s",s+1);
    len=strlen(s+1);
    For(i,1,len) add_char(i);
    For(i,1,tot) ++sum[maxlen[i]];
    For(i,1,len) sum[i]+=sum[i-1];
    For(i,1,tot) id[sum[maxlen[i]]--]=i;
    Forr(i,tot,1){
        int x=id[i];
        f[x]=1;
        For(j,0,25) f[x]+=f[tree[x][j]];
    }
    int m,k;
    scanf("%d",&m);
    while(m--){
        scanf("%d",&k);
        int p=1;
        while(k)
            For(i,0,25){
                if(tree[p][i]){
                    if(f[tree[p][i]]>=k){
                        p=tree[p][i];
                        putchar('a'+i);
                        --k;
                        break;
                    }
                    else k-=f[tree[p][i]];
                }
            }
        puts("");
    }
    return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值