首页 > 技术文章 > 【TJOI2015】弦论 (后缀数组)

ezoiLZH 2018-08-13 21:53 原文

前言:

多好的题啊!

我理论$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 }

推荐阅读