BZOJ4310跳蚤——后缀数组

Description
很久很久以前,森林里住着一群跳蚤。一天,跳蚤国王得到了一个神秘的字符串,它想进行研究。首先,他会把串
分成不超过 k 个子串,然后对于每个子串 S,他会从S的所有子串中选择字典序最大的那一个,并在选出来的 k
个子串中选择字典序最大的那一个。他称其为“魔力串”。现在他想找一个最优的分法让“魔力串”字典序最小。

Input
第一行一个整数 k,K<=15
接下来一个长度不超过 10^5 的字符串 S。

Output
输出一行,表示字典序最小的“魔力串”。

Sample Input

2
ababa

Sample Output

ba

//解释:

分成aba和ba两个串,其中字典序最大的子串为ba


既然要求最大的最小,我们显然可以考虑二分答案的思想。

在做这道题之前,我们需要普及一些知识(了解的可以直接略过去看题解)
之前我们讲了后缀数组以及其简单的一些应用,但是单单一个sa数组其实没什么用。我们还有两个附带的数组rank数组和height数组。其中rank表示的是后缀i在sa中的下标,我们可以用一段简单的程序表示:
for(int i=0;i<n;i++) rank[sa[i]]=i;
而height数组表示的是sa[i-1]和sa[i]的LCP(longest common prefix,最长公共前缀)的长度。对于两个后缀j和k,假设 rank[j]<rank[k] r a n k [ j ] < r a n k [ k ] ,那么很容易知道j和k的LCP为 min(height[rank[j]+1],height[rank[j]+2]height[rank[k]]) m i n ( h e i g h t [ r a n k [ j ] + 1 ] , h e i g h t [ r a n k [ j ] + 2 ] … … h e i g h t [ r a n k [ k ] ] )
这个非常容易证明,大家可以自己写一个字符串,把它后缀排序,然后自己手算,就可以发现它的正确性(这里我就偷一个小懒,不举例子了,因为底下还有很多需要举例子的地方
所以我们可以用RMQ来求某两个子串的LCP。
下面给出求height的代码:
int k=0;
for(int i=0;i<n;i++) rank[sa[i]]=i;
for(int i=0;i<n;i++){
    if(k) k--;
    int j=sa[rank[i]-1];
    while(s[i+k]==s[j+k]) k++;
    height[rank[i]]=k;
}

好了,回到本题。
首先我们要知道所有本质不同的子串的个数。我们可以根据公式 sum=nsa[i]height[i] s u m = ∑ n − s a [ i ] − h e i g h t [ i ] 求出其本质不同的子串个数( sum s u m ),但这个公式是为什么呢?给大家举个例子就懂了:
比如我们要求 ababa a b a b a 的本质不同的子串。我们先按后缀排序,得到 a,aba,ababa,ba,baba a , a b a , a b a b a , b a , b a b a
所以我们的后缀数组是 (4,2,0,3,1) ( 4 , 2 , 0 , 3 , 1 ) ,那么我们求出height数组 (0,1,3,0,2) ( 0 , 1 , 3 , 0 , 2 )
我们知道 nsa[i] n − s a [ i ] 也就是我们当前后缀i的长度。所以这个后缀后 nsa[i] n − s a [ i ] 个前缀。然而并不是所有的前缀都能构成新的本质不同的子串。我们知道从1到height[i]的这些前缀都已经出现在i-1中了。所以重复了,要减去。所以最后的 sum s u m 就是 nsa[i]height[i] ∑ n − s a [ i ] − h e i g h t [ i ] ,比如我们后缀baba,它的前缀是 baba,bab,ba,b b a b a , b a b , b a , b ,但是 ba b a 是它和上一个串的LCP,所以 ba,b b a , b 已经在上面的字符串(也就是 ba b a )中出现过了。所以要减去。
接下去我们就进行二分,二分我们答案串的排名(假设我们把所有串的按照字典序排序)。那么怎么查找我们对应的串呢?假设我们要查找排名为 k k 的字符串,我们考虑累计sum的过程,对于每一个后缀,我们都求出其前缀且本质不同的串。所以这些串的字典序是连续的。所以只要我们按照后缀数组的顺序去找,即可找到排名为 k k 的子串。
void getPL(ll x){
    for(int i=0;i<n;i++)
     if(x>n-sa[i]-height[i]) x-=n-sa[i]-height[i];  //如果加上当前后缀所能产生的字串数依旧小于我们要求的子串,那么就继续枚举
     else{
        ls=sa[i],rs=sa[i]+height[i]+x-1;//否则即可找出子串
        return;
     }
}
那么剩下的问题就是怎么check了。我们从后往前枚举,考虑哪些情况必须让我们割一刀:
首先第一种情况就是出现了一个字符s,它的字典序大于我们枚举的子串的第一个字符。如果出现这种情况,那我们直接return 0即可。因为肯定最终会选有关s的子串而不是目前的子串。
第二种情况就是出现了一个后缀,它在后缀数组中的排名大于目前的子串。那么我们必须把这个后缀截成两段,不让它出现。如果我们截的次数大于题目要求的k,那么当前的子串肯定也是不可行的。所以我们从后往前枚举的时候还要比较当前后缀和我们二分出的子串的字典序大小。那我们只要比较它们LCP后面的第一个字符即可。
int compare(int x1,int y1,int x2,int y2){
    int l1=y1-x1+1,l2=y2-x2+1;
    int lcp=LCP(rank[x1],rank[x2]);
    if(lcp>=l1) return l1<=l2;
    else if(lcp>=l2) return 0;
    else return s[x1+lcp]<=s[x2+lcp];
}
int check(){
    int last=n-1,p=1;
    for(int i=n-1;i>=0;i--){
        if(s[i]>s[ls]) return 0;
        else if(!compare(i,last,ls,rs)){
            p++;last=i;
            if(p>k) return 0;
        }
    }
    return 1;
}
CODE:
#include<bits/stdc++.h>
#define MAXN 100005
#define ll long long
using namespace std;
int read(){
    char c;int x;while(c=getchar(),c<'0'||c>'9');x=c-'0';
    while(c=getchar(),c>='0'&&c<='9') x=x*10+c-'0';return x;
}
int k,n,p[MAXN],sa[MAXN],t[MAXN],t1[MAXN],c[MAXN],rank[MAXN],height[MAXN],rmq[MAXN][18];
int Lans,Rans,ls,rs;
ll sum,l,r,mid;
string s;
void getSA(int m){
    int *A=t,*B=t1;
    for(int i=0;i<m;i++) c[i]=0;
    for(int i=0;i<n;i++) c[A[i]=p[i]]++;
    for(int i=0;i<m;i++) c[i]+=c[i-1];
    for(int i=n-1;i>=0;i--) sa[--c[A[i]]]=i;
    for(int k=1;k<=n;k<<=1){
        int p=0;
        for(int i=n-k;i<n;i++) B[p++]=i;
        for(int i=0;i<n;i++) if(sa[i]>=k) B[p++]=sa[i]-k;
        for(int i=0;i<m;i++) c[i]=0;
        for(int i=0;i<n;i++) c[A[B[i]]]++;
        for(int i=0;i<m;i++) c[i]+=c[i-1];
        for(int i=n-1;i>=0;i--) sa[--c[A[B[i]]]]=B[i];
        swap(A,B);
        p=1,A[sa[0]]=0;
        for(int i=1;i<n;i++) A[sa[i]]=(B[sa[i-1]]==B[sa[i]]&&B[sa[i-1]+k]==B[sa[i]+k])?p-1:p++;
        if(p==n) break;m=p;
    }
}
void getHEIGHT(){
    int k=0;
    for(int i=0;i<n;i++) rank[sa[i]]=i;
    for(int i=0;i<n;i++){
        if(k) k--;
        int j=sa[rank[i]-1];
        while(s[i+k]==s[j+k]) k++;
        height[rank[i]]=k;
    }
}
void getPL(ll x){
    for(int i=0;i<n;i++)
     if(x>n-sa[i]-height[i]) x-=n-sa[i]-height[i];
     else{
        ls=sa[i],rs=sa[i]+height[i]+x-1;
        return;
     }
}
int LCP(int a,int b){
    if(a==b) return n-sa[a];
    if(a>b) swap(a,b);
    int k=log2(b-a);
    return min(rmq[a+1][k],rmq[b-(1<<k)+1][k]);
}
int compare(int x1,int y1,int x2,int y2){
    int l1=y1-x1+1,l2=y2-x2+1;
    int lcp=LCP(rank[x1],rank[x2]);
    if(lcp>=l1) return l1<=l2;
    else if(lcp>=l2) return 0;
    else return s[x1+lcp]<=s[x2+lcp];
}
int check(){
    int last=n-1,p=1;
    for(int i=n-1;i>=0;i--){
        if(s[i]>s[ls]) return 0;
        else if(!compare(i,last,ls,rs)){
            p++;last=i;
            if(p>k) return 0;
        }
    }
    return 1;
}
int main()
{
    k=read();cin>>s;n=s.size();sa[-1]=-1;
    for(int i=0;i<n;i++) p[i]=s[i]-'a'+1;
    getSA(28);getHEIGHT();
    for(int i=0;i<n;i++) sum+=(n-sa[i]-height[i]);
    for(int i=0;i<n;i++) rmq[i][0]=height[i];
    for(int j=1;j<=17;j++)
     for(int i=0;i+(1<<j)-1<n;i++) rmq[i][j]=min(rmq[i][j-1],rmq[i+(1<<j-1)][j-1]);
    l=1,r=sum;
    while(l<=r){
        mid=(l+r)>>1;
        getPL(mid);
        if(check()) Lans=ls,Rans=rs,r=mid-1;
        else l=mid+1;
    }
    for(int i=Lans;i<=Rans;i++) putchar(s[i]);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值