题解 P3975 【[TJOI2015]弦论】
没人发后缀数组的做法,我来发一波!
戳这里食用更佳ヾ(◍°∇°◍)ノ゙
相信T=0,大家都会做,这是后缀数组的一个经典问题。
具体就是,每个子串都是一个后缀的前缀。
从头到尾扫,每个后缀都会有 n−sa[i]+1−height[i] 个本质不同的子串,即可。
重点是T=1时,怎么用后缀数组解决?
由于我比较菜,我想了一个大暴力,一位一位的枚举!
因为我们前面的字母是确定的,那么当前面的字母一样的时候,后面一个字母一定是单调不下降的。
那我们就能二分求出这个字母最后一个的位置。
我们建一个后缀长度的前缀和,那我们就能求出以这个字母开头的子串有多少个。
当个数大于k时,就确定了这个字母,否则k减去,继续枚举下一个字母。
枚举完一位继续下一位,那我们就能把范围缩小,知道求出答案。
具体看代码……
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,t,k;
long long sum[500005];
char s[500005];
struct SA{
char s[500005];
int tp[500005],rak[500005];
int tax[500005],sa[500005];
int n,m,height[500005];
void build(char str[]){
memcpy(s,str,sizeof(s));
n=strlen(s+1);
build_sa(rak,tp);
build_height();
}
void sort(int a[],int b[]){
for(int i=1;i<=m;i++)tax[i]=0;
for(int i=1;i<=n;i++)tax[a[i]]++;
for(int i=1;i<=m;i++)tax[i]+=tax[i-1];
for(int i=n;i>=1;i--)sa[tax[a[b[i]]]--]=b[i];
}
bool comp(int r[],int a,int b,int k){
return r[a]==r[b]&&r[a+k]==r[b+k];
}
void build_sa(int a[],int b[]){
for(int i=1;i<=n;i++)
m=max(m,a[i]=s[i]-'a'+1),b[i]=i;
sort(a,b);
for(int p=0,j=1;p<n;j<<=1,m=p){
p=0;
for(int i=1;i<=j;i++)b[++p]=n-j+i;
for(int i=1;i<=n;i++)if(sa[i]>j)b[++p]=sa[i]-j;
sort(a,b);
int *t=a;a=b;b=t;
a[sa[1]]=p=1;
for(int i=2;i<=n;i++)
a[sa[i]]=comp(b,sa[i],sa[i-1],j)?p:++p;
}
for(int i=1;i<=n;i++)rak[sa[i]]=i;
}
void build_height(){
for(int i=1,j=0;i<=n;i++){
if(j)j--;
while(s[i+j]==s[sa[rak[i]-1]+j])j++;
height[rak[i]]=j;
}
}
}a;
int main(){
scanf("%s%d%d",s+1,&t,&k);
a.build(s);
n=strlen(s+1);
if(t==0){
for(int i=1;i<=n;i++){
int c=n-a.sa[i]+1-a.height[i];
if(k<=c){
for(int j=a.sa[i];j<=a.sa[i]+a.height[i]+k-1;j++)
putchar(s[j]);
return 0;
}else k-=c;
}
printf("-1");
}else{
for(int i=1;i<=n;i++) //处理前缀和
sum[i]=sum[i-1]+n-a.sa[i]+1;
if(sum[n]<k)return printf("-1"),0; //子串不够输出-1
int L=1,R=n;
for(int i=1;i<=n;i++){
int tmp=L;
for(int j='a';j<='z';j++){ //枚举 a~z
int l=tmp,r=R;
while(l<=r){ //二分找这个字母的最后一个位置
int mid=l+r>>1;
if(s[a.sa[mid]+i-1]>j)r=mid-1;
else l=mid+1;
}
long long t=sum[r]-sum[tmp-1]-1LL*(r-tmp+1)*(i-1);
//现在枚举的区间有多少个子串
//减是因为减去前面枚举过得位置
if(k<=r-tmp+1){
//现在要查的比现在字母的个数少,说明这个字母就是结束的位置
for(int j=a.sa[tmp];j<=a.sa[tmp]+i-1;j++)
putchar(s[j]);
return 0;
}
if(t>=k){ //说明这位就是这个字母,减去字母个数
L=tmp,R=r;
k-=r-tmp+1;
break;
}
tmp=r+1,k-=t; //不是,继续枚举
}
if(n-a.sa[L]+1==i)L++; //如果下一位为空,就不用算了。
}
}
}