BZOJ3998[TJOI2015]弦论
Description
对于一个给定长度为$N$的字符串,求它的第$K$小子串是什么。
[1]不同位置的相同子串算作$1$个;
[2]不同位置的相同子串算作多个;
Input
第一行是一个仅由小写英文字母构成的字符串$S$
第二行为两个整数$T$和$K$,$T$为$0$则表示不同位置的相同子串算作一个。$T=1$则表示不同位置的相同子串算作多个。$K$的意义如题所述。
Output
输出仅一行,为一个数字串,为第$K$小的子串。如果子串数目不足$K$个,则输出$-1$
题解
模拟赛里写的时候本蒟蒻并不会后缀自动机的做法,然而后缀数组的方法我还是会的...
对雅礼集训水箱一题和2018洛谷7月月赛beautiful pair这两题的类似分治一样的方法的总结,这种思路对我而言还是很好想到并实现的。
首先建出后缀数组...
然后,按排名找到两两之间最小的$height$值,如果相同取排名更靠前的,这样也就找出了最长的每一个后缀都有的一个相同的前缀$S$。由于后缀数组的性质,这个$S$的每一个前缀一定恰好是所有子串中字典序最小的那一部分字符串。这时候这一做法比后缀自动机更优的地方就体现出来了,对于$T=0$和$T=1$的差异,唯一的差别是,当$T=0$时,$S$的每一个前缀都对答案有$1$的贡献,当$T=1$时,$S$的每一个前缀都对答案有$N$的贡献。如果我们发现这些$S$的前缀不足以成为第$K$大的子串,那么就将$K$减去所有这些$S$的前缀的贡献之和,再优先递归处理排名更靠前的区间,接着再处理另一个区间。
注意,递归时要新增一参数$last$,表示上一级处理出的$S$为$S'$,因为当我们处理出当前区间的$S$时,有意义的部分只有每一个长度大于$|S'|$的$S$的前缀,因为$S'$的前缀已经在上一级被统计过了。
复杂度证明:
和水箱那题的方法如出一辙,不过为了保持新鲜感,还是换个方法。
后缀数组本身是$O(N\cdot log(N))$,使用线段树维护$height$最小值出现的位置,每次查询为$O(log(N))$,一共查询的次数是长度大于一的区间,每个区间由两个子区间构成。想象一下,初始有$N$个长度为$1$的区间,每次选出两个合并,产生一个新的长度大于$1$的区间,同时总的区间的数量$-1$,这样一共会产生$N-1$个长度大于$1$的区间。而一定有一种合并的方案,使得每次产生出的区间恰好对应每一个要查询的区间,这样的区间恰好有$N-1$个,所以复杂度$O(N\cdot log(N))$。
于是总复杂度$O(N\cdot log(N))$。
然而模拟赛中,写$init$函数和$solve$函数的顺序看着别扭,全选了一个函数在Dev C++ 5.1.1中用$Ctrl+Shift+\uparrow \downarrow$进行整体上移下移,随后又瞎按了几下$Ctrl+Z$(撤销)和$Ctrl+Y$(重做)导致后缀数组的某一个部分出现了两遍最后交上去极其惨烈......
AC代码如下
#include<algorithm> #include<iostream> #include<cstring> #include<cstdio> #include<cmath> #define LL long long #define mid (l+r>>1) #define M 500020 using namespace std; int read(){ int nm=0,fh=1;char cw=getchar(); for(;!isdigit(cw);cw=getchar()) if(cw=='-') fh=-fh; for(;isdigit(cw);cw=getchar()) nm=nm*10+(cw-'0'); return nm*fh; } int n,m=124,rk[M],sa[M],pos[M],c[M],height[M],T,K; int p[M<<2]; char ch[M]; void finish(int l,int r){ for(int i=l;i<=r;i++) putchar(ch[i]); putchar('\n'); exit(0); } void build(int x, int l,int r){ if(l==r){p[x]=l;return;} build(x<<1,l,mid),build(x<<1|1,mid+1,r); int t1=p[x<<1],t2=p[x<<1|1]; if(height[t1]<=height[t2]) p[x]=t1; else p[x]=t2; } int query(int x,int l,int r,int L,int R){ if(L<=l&&r<=R) return p[x]; if(R<l||r<L) return 1; int t1=query(x<<1,l,mid,L,R); int t2=query(x<<1|1,mid+1,r,L,R); if(height[t1]<=height[t2]) return t1; else return t2; } void init(){ scanf("%s",ch+1),n=strlen(ch+1),T=read(),K=read(); for(int i=1;i<=n;i++) c[rk[i]=ch[i]]++; for(int i=2;i<=m;i++) c[i]+=c[i-1]; for(int i=n;i>=1;i--) sa[c[rk[i]]--]=i; for(int k=1;k<=n;k<<=1){ int tot=0; for(int i=n-k+1;i<=n;i++) pos[++tot]=i; for(int i=1;i<=n;i++) if(sa[i]>k) pos[++tot]=sa[i]-k; for(int i=1;i<=m;i++) c[i]=0; for(int i=1;i<=n;i++) c[rk[i]]++; for(int i=2;i<=m;i++) c[i]+=c[i-1]; for(int i=n;i>=1;i--) sa[c[rk[pos[i]]]--]=pos[i]; swap(pos,rk),tot=rk[sa[1]]=1; for(int i=2;i<=n;i++) rk[sa[i]]=(pos[sa[i]]==pos[sa[i-1]]&&pos[sa[i]+k]==pos[sa[i-1]+k]?tot:++tot); if((m=tot)==n) break; } for(int i=1,k=0;i<=n;i++){ if(k) k--; if(rk[i]==1) continue; for(int j=sa[rk[i]-1];i+k<=n&&j+k<=n&&ch[j+k]==ch[i+k];k++); height[rk[i]]=k; } height[1]=n,build(1,1,n); } void solve(int l,int r,int last){ if(l==r){ if(K<=n-sa[l]+1-last) finish(sa[l],sa[l]+last+K-1); else{K-=n-sa[l]+1-last;return;} } int now=query(1,1,n,l+1,r),res=T==0?1:r-l+1; int dt=res*(height[now]-last); if(K>dt&&dt>=0) K-=dt,solve(l,now-1,height[now]),solve(now,r,height[now]); else{for(K>res;K-=res,last++;); finish(sa[r],sa[r]+last);} } int main(){init(),solve(1,n,0),puts("-1"); return 0;}