[TJOI2015]弦论(后缀数组or后缀自动机)

解法一:后缀数组

听说后缀数组解第k小本质不同的子串是一个经典问题。
把后缀排好序后第i个串的本质不同的串的贡献就是(n-sa[i]+1-LCP(i,i-1))然后我们累加这个贡献,看到哪一个串的时候,这个贡献的和大于等于k,然后答案就在这个串里了,然后枚举就行了。
那么第k小子串该怎么办?
我们考虑二分答案,我们按字典序大小二分一个子串(具体就是二分第k小的本质不同子串,因为这个串可以(O(n))求),然后看看比这个串小的串有多少个?然后改变上下界就行了。
那么我们如何求出比一个串小的串有多少个?
设我们我们二分的子串是后缀数组排名为x的后缀的前缀,长度为len。贡献就是(sum_{i=1}^{x-1}n-sa[i]+1+sum_{i=x}^{n}min(LCP(x,i),len))
然后这个题就解决了。
代码很丑

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=501000;
int c[N],x[N],sa[N],y[N],height[N],rk[N],n,m,t,k,tmp,ans;
char s[N];
int read(){
    int sum=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();}
    return sum*f;
}
void get_sa(){
    for(int i=1;i<=n;i++)c[x[i]=s[i]]++;
    for(int i=1;i<=m;i++)c[i]+=c[i-1];
    for(int i=n;i>=1;i--)sa[c[x[i]]--]=i;
    for(int k=1;k<=n;k<<=1){
        int num=0;
        for(int i=n-k+1;i<=n;i++)y[++num]=i;
        for(int i=1;i<=n;i++)if(sa[i]>k)y[++num]=sa[i]-k;
        for(int i=1;i<=m;i++)c[i]=0;
        for(int i=1;i<=n;i++)c[x[i]]++;
        for(int i=1;i<=m;i++)c[i]+=c[i-1];
        for(int i=n;i>=1;i--)sa[c[x[y[i]]]--]=y[i],y[i]=0;
        for(int i=1;i<=n;i++)swap(x[i],y[i]);
        x[sa[1]]=1;num=1;
        for(int i=2;i<=n;i++)
            x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?num:++num;
        if(n==num)return;
        m=num;
    }
}
void get_height(){
    int k=0;
    for(int i=1;i<=n;i++)rk[sa[i]]=i;
    for(int i=1;i<=n;i++){
        if(rk[i]==1)continue;
        if(k)k--;
        int j=sa[rk[i]-1];
        while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++;
        height[rk[i]]=k;
    }
}
int judge(int x){
    int num=0;
    tmp=0;
    for(int i=1;i<=n;i++){
        if(tmp+n-sa[i]+1-height[i]>=x){
            int len=0;
            for(int j=sa[i];j-sa[i]+1-height[i]<=x-tmp;j++)len++;
            int mn=height[i+1];
            num+=len;
            for(int j=i+1;j<=n;j++){
                mn=min(height[j],mn);
                if(height[j]<len){
                    for(int k=j;k<=n;k++){
                        mn=min(height[k],mn);
                        num+=mn;
                    }
                    return num;
                }
                num+=len;
            }
        }
        num+=n-sa[i]+1;
        tmp=tmp+n-sa[i]+1-height[i];
    }
}
int main(){
    scanf("%s",s+1);
    n=strlen(s+1);
    m=122;
    get_sa();get_height();
    t=read();k=read();
    if(n*(n+1)/2<k){
        printf("-1");
        return 0;
    }
    if(t==0){
        for(int i=1;i<=n;i++){
            if(tmp+n-sa[i]+1-height[i]>=k){
                for(int j=sa[i];j-sa[i]+1-height[i]<=k-tmp;j++)printf("%c",s[j]);
                return 0;
            }
            tmp=tmp+n-sa[i]+1-height[i];
        }
    }
    else{
        int l=1,r=k;
        while(l<=r){
            int mid=(l+r)>>1;
            if(judge(mid)>=k){
                ans=mid;
                r=mid-1;
            }
            else l=mid+1;
        }
        tmp=0;
        for(int i=1;i<=n;i++){
            if(tmp+n-sa[i]+1-height[i]>=ans){
                for(int j=sa[i];j-sa[i]+1-height[i]<=ans-tmp;j++)printf("%c",s[j]);
                return 0;
            }
            tmp=tmp+n-sa[i]+1-height[i];
        }
    }
    return 0;
}

解法二 后缀自动机

表示后缀自动机根本不会用。555
trans数组看做边的话一个(DAG),从这个(root)出发的每一条路径对应原串的一个子串这些子串都是本质不同的。我们可以做一个DP求出从一个点出发的所有路径有多少条路径转移方程(dp[u]=1+sum dp[v])。然后再在图上像类似线段树上二分的方法就可以求出答案了。
那么第二问该怎么办?
我们注意到一个串出现的次数就是后缀树中这个节点的子树内的后缀节点数(就是代表一个串结束的节点数)。所以我们可以仿照第一问的方案,只不过DP的方程改为了(dp[u]=size[u]+sum dp[v])(这里的(size[u])代表后缀树中(u)的子树的后缀节点数)

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=1001000;
int tot=1,u=1,len[N],size[N],fa[N],trans[N][27],n,t,k,f[N],c[N],A[N];;
bool vis[N];
char s[N];
void ins(int c){
    int x=++tot;size[x]=1;
    len[x]=len[u]+1;
    for(;u&&trans[u][c]==0;u=fa[u])trans[u][c]=x;
    if(u==0)fa[x]=1;
    else{
        int v=trans[u][c];
        if(len[u]+1==len[v])fa[x]=v;
        else{
            int w=++tot;
            len[w]=len[u]+1; 
            memcpy(trans[w],trans[v],sizeof(trans[w]));fa[w]=fa[v];
            fa[v]=fa[x]=w;
            for(;u&&trans[u][c]==v;u=fa[u])trans[u][c]=w;
        }
    }
    u=x;
}
void work(int x,int k){
    if(k<=size[x]) return;
    k-=size[x];
    for(int i=1;i<=26;i++){
        int R=trans[x][i]; if(!R) continue;
        if(k>f[R]) {k-=f[R];continue;}
        putchar(i+'a'-1);work(R,k);return;
    }
}
int read(){
    int sum=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();}
    return sum*f;
}
int main(){
    scanf("%s",s+1);
    n=strlen(s+1);
    for(int i=1;i<=n;i++)ins(s[i]-'a'+1);
    t=read();k=read();
    if(n*(n+1)/2<k){printf("-1");return 0;}
    for(int i=1;i<=tot;i++)c[len[i]]++;
    for(int i=1;i<=tot;i++)c[i]+=c[i-1];
    for(int i=1;i<=tot;i++)A[c[len[i]]--]=i;
    for(int i=tot;i>=1;i--)size[fa[A[i]]]+=size[A[i]];
    for(int i=1;i<=tot;i++)t==0?(f[i]=size[i]=1):(f[i]=size[i]);
    size[1]=f[1]=0;
    for(int i=tot;i>=1;i--)
        for(int j=1;j<=26;j++)
            if(trans[A[i]][j])f[A[i]]+=f[trans[A[i]][j]];
    work(1,k);
    return 0;
}

解法三:后缀树

也是类似线段树二分的思想跟SAM差不多,不过不是在图里二分了,在树上二分。

原文地址:https://www.cnblogs.com/Xu-daxia/p/10203608.html