JZOJ 4072. 【TJOI2015】弦论

题目

给定一个长度为 N N 的字符串|S|,有两种问法。
第一种:问第 K K 小的本质不同的子串。
第二种:问第K小的子串。
本质不同,也就是说如果同一个子串在不同的位置出现了,不重复计算。
N105 N ≤ 10 5

题解

所谓子串,是后缀的前缀。
Trie?Trie不能够在复杂度允许的情况下识别所有的子串。
SA?对于第一种问法显然可以做。但是搞不定第二种。
那么有没有什么能够一下子识别所有的子串,且能够维护相同的子串出现的次数?
可以用SAM。
在SAM的转移图中,从起点走到转移图中的任一点的路径,表示任一个子串。每个点的 right r i g h t 集恰好表示了一个子串出现的次数。
那么怎么 O(|S|) O ( | S | ) 维护 right r i g h t 集的大小?
sum[x] s u m [ x ] 表示 x x 点的right集大小。
一开始在建SAM时, sum[np]=1 s u m [ n p ] = 1 ,表示新增状态的 right r i g h t 集中的元素增加了 |S| | S | 一个元素。
利用 fail f a i l 树, sum[fail[x]]+=sum[x] s u m [ f a i l [ x ] ] + = s u m [ x ] ,表示 fail[x] f a i l [ x ] 对应的子串的出现次数必然包含 x x 对应的子串的出现次数。
right[fail[x]]right[x],任意的 fail[x]=fail[y] f a i l [ x ] = f a i l [ y ] right[x] r i g h t [ x ] right[y] r i g h t [ y ] 的任意元素都互不相同。所以以上的算法是正确的。
su[x] s u [ x ] 表示以 x x 对应的子串为前缀的所有子串在S中出现的次数,则像权值线段树那样子跑一遍求出具体的答案。
对于第一种问法?
强行让 sum[x]=1 s u m [ x ] = 1 ,其他步骤按照上面做。

代码

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#define N 500010
#define M 1000010
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
using namespace std;
struct note{
    int to,next;
};note edge[M];
int tot,head[M];
int i,j,k,K,l,r,x,n,m,t,temp;
int gs,wz;
int Len[M],fail[M],qu[M];
int sum[M],su[M],o[M];
int sam[M][26];
char ch,s[N];
void lb(int x,int y){edge[++tot].to=y;edge[tot].next=head[x];head[x]=tot;}
int ins(int x,int wz){
    int i,np=++gs,p=wz,nq,q;
    Len[np]=Len[p]+1;sum[np]++;
    while(p&&!sam[p][x])sam[p][x]=np,p=fail[p];
    if(!p){fail[np]=1;return np;}
    q=sam[p][x];
    if(Len[q]==Len[p]+1)fail[np]=q;else{
        nq=++gs;
        fo(i,0,25)sam[nq][i]=sam[q][i];
        fail[nq]=fail[q];
        fail[np]=fail[q]=nq;
        Len[nq]=Len[p]+1;
        while(p&&sam[p][x]==q)sam[p][x]=nq,p=fail[p];
    }
    return np;
}
int main(){
    freopen("string.in","r",stdin);
    freopen("string.out","w",stdout);
    ch=getchar();
    while(ch>='a'&&ch<='z')s[++n]=ch,ch=getchar();
    scanf("%d%d",&t,&K);
    gs=wz=1;
    fo(i,1,n)
        wz=ins(s[i]-'a',wz);
    if(!t)fo(i,2,gs)sum[i]=1;else{
        fo(i,2,gs)lb(fail[i],i);
        qu[r=1]=1;l=0;
        while(l<r){
            x=qu[++l];
            for(i=head[x];i;i=edge[i].next)qu[++r]=edge[i].to;
        }
        fd(i,gs,2)sum[fail[qu[i]]]+=sum[qu[i]];
        sum[1]=0;
    }
    fo(i,1,gs)fo(j,0,25)if(sam[i][j])o[sam[i][j]]++;
    qu[r=1]=1;l=0;
    while(l<r){
        i=qu[++l];
        fo(j,0,25)if(sam[i][j]){
            o[sam[i][j]]--;
            if(!o[sam[i][j]])qu[++r]=sam[i][j];
        }
    }
    fd(i,gs,1){
        fo(j,0,25)su[qu[i]]+=su[sam[qu[i]][j]];
        su[qu[i]]+=sum[qu[i]];
    }
    if(K>su[1]){puts("-1");return 0;}
    wz=1;
    while(1){
        if(sum[wz]>=K)break;
        temp=sum[wz];
        fo(i,0,25)
            if(temp+su[sam[wz][i]]<K)temp+=su[sam[wz][i]];
            else break;
        putchar(i+'a');
        K-=temp;
        wz=sam[wz][i];
    }
    return 0;
}
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值