【TJOI2015】弦论 (后缀数组)
前言:
多好的题啊!
我理论$O(nlog_2n)$的后缀数组还带个常数26,竟然跑的比$O(n)$的后缀自动机还快,全场 Rak 1?
Description
为了提高智商,ZJY开始学习弦论。这一天,她在《 String theory》中看到了这样一道问题:对于一个给定的长度为n的字符串,求出它的第k小子串是什么。你能帮帮她吗?
Input
第一行是一个仅由小写英文字母构成的字符串s
第二行为两个整数t和k,t为0则表示不同位置的相同子串算作一个,t为1则表示不同位置的相同子串算作多个。k的意义见题目描述。
Output
输出数据仅有一行,该行有一个字符串,为第k小的子串。若子串数目不足k个,则输出-1。
题解:
相信T=0,大家都会做,这是后缀数组的一个经典问题。
具体就是,每个子串都是一个后缀的前缀。
从头到尾扫,每个后缀都会有 $n-sa[i]+1-height[i]$ 个本质不同的子串,即可。
重点是T=1时,怎么用后缀数组解决?
由于我比较菜,我想了一个大暴力,一位一位的枚举!
因为我们前面的字母是确定的,那么当前面的字母一样的时候,后面一个字母一定是单调不下降的。
那我们就能二分求出这个字母最后一个的位置。
我们建一个后缀长度的前缀和,那我们就能求出以这个字母开头的子串有多少个。
当个数大于$k$时,就确定了这个字母,否则$k$减去,继续枚举下一个字母。
枚举完一位继续下一位,那我们就能把范围缩小,知道求出答案。
具体看代码……
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 6 int n,t,k; 7 long long sum[500005]; 8 char s[500005]; 9 struct SA{ 10 char s[500005]; 11 int tp[500005],rak[500005]; 12 int tax[500005],sa[500005]; 13 int n,m,height[500005]; 14 void build(char str[]){ 15 memcpy(s,str,sizeof(s)); 16 n=strlen(s+1); 17 build_sa(rak,tp); 18 build_height(); 19 } 20 void sort(int a[],int b[]){ 21 for(int i=1;i<=m;i++)tax[i]=0; 22 for(int i=1;i<=n;i++)tax[a[i]]++; 23 for(int i=1;i<=m;i++)tax[i]+=tax[i-1]; 24 for(int i=n;i>=1;i--)sa[tax[a[b[i]]]--]=b[i]; 25 } 26 bool comp(int r[],int a,int b,int k){ 27 return r[a]==r[b]&&r[a+k]==r[b+k]; 28 } 29 void build_sa(int a[],int b[]){ 30 for(int i=1;i<=n;i++) 31 m=max(m,a[i]=s[i]-'a'+1),b[i]=i; 32 sort(a,b); 33 for(int p=0,j=1;p<n;j<<=1,m=p){ 34 p=0; 35 for(int i=1;i<=j;i++)b[++p]=n-j+i; 36 for(int i=1;i<=n;i++)if(sa[i]>j)b[++p]=sa[i]-j; 37 sort(a,b); 38 int *t=a;a=b;b=t; 39 a[sa[1]]=p=1; 40 for(int i=2;i<=n;i++) 41 a[sa[i]]=comp(b,sa[i],sa[i-1],j)?p:++p; 42 } 43 for(int i=1;i<=n;i++)rak[sa[i]]=i; 44 } 45 void build_height(){ 46 for(int i=1,j=0;i<=n;i++){ 47 if(j)j--; 48 while(s[i+j]==s[sa[rak[i]-1]+j])j++; 49 height[rak[i]]=j; 50 } 51 } 52 }a; 53 54 int main(){ 55 scanf("%s%d%d",s+1,&t,&k); 56 a.build(s); 57 n=strlen(s+1); 58 if(t==0){ 59 for(int i=1;i<=n;i++){ 60 int c=n-a.sa[i]+1-a.height[i]; 61 if(k<=c){ 62 for(int j=a.sa[i];j<=a.sa[i]+a.height[i]+k-1;j++) 63 putchar(s[j]); 64 return 0; 65 }else k-=c; 66 } 67 printf("-1"); 68 }else{ 69 for(int i=1;i<=n;i++) //处理前缀和 70 sum[i]=sum[i-1]+n-a.sa[i]+1; 71 if(sum[n]<k)return printf("-1"),0; //子串不够输出-1 72 int L=1,R=n; 73 for(int i=1;i<=n;i++){ 74 int tmp=L; 75 for(int j='a';j<='z';j++){ //枚举 a~z 76 int l=tmp,r=R; 77 while(l<=r){ //二分找这个字母的最后一个位置 78 int mid=l+r>>1; 79 if(s[a.sa[mid]+i-1]>j)r=mid-1; 80 else l=mid+1; 81 } 82 long long t=sum[r]-sum[tmp-1]-1LL*(r-tmp+1)*(i-1); 83 //现在枚举的区间有多少个子串 84 //减是因为减去前面枚举过得位置 85 if(k<=r-tmp+1){ 86 //现在要查的比现在字母的个数少,说明这个字母就是结束的位置 87 for(int j=a.sa[tmp];j<=a.sa[tmp]+i-1;j++) 88 putchar(s[j]); 89 return 0; 90 } 91 if(t>=k){ //说明这位就是这个字母,减去字母个数 92 L=tmp,R=r; 93 k-=r-tmp+1; 94 break; 95 } 96 tmp=r+1,k-=t; //不是,继续枚举 97 } 98 if(n-a.sa[L]+1==i)L++; //如果下一位为空,就不用算了。 99 } 100 } 101 }