JSK 习题:蒜头君传纸条进阶版(hdu 5008)-后缀数组,求第k大子串的左右端点

在这里插入图片描述
在这里插入图片描述

题意:

给出一个字符串,求其第k大子串出现的位置,即其左右端点下标

思路:

首先构建后缀数组,因为后缀数组会对每个后缀排序,就相当于对所有子串进行了排序,因为子串就是后缀的前缀

对于第i个后缀,其含有不同的子串个数为n-sa[i]-height[i]
我们定义sum[i]=sum[i-1]+n-sa[i]-height[i],求出sum[1n],即求出第1n个后缀所含有不同子串个数的前缀和

如何找第k大子串,对于第一个sum[x]>=k,则第k大子串一定是以sa[x]开头的后缀的一个前缀
由此我们可以确定第k大子串的长度,len=k-sum[x-1]+height[x]
可以这样理解,k表示在整个串中第k大的子串,t=k-sum[x-1]即表示在以sa[x]开头的后缀的第t个前缀就是整个串中第k大子串
但这样算出的第t个前缀可能也在其他后缀中出现,就会产生重复,所以再加上height[x]用来去重,这样算出的len就是第k大子串的长度

我们求出第k大子串第一次出现在第x个后缀中,但并不表示它第一次出现在整个字符串中,因为后缀顺序与真实顺序并不一样
若第k大子串有多个,那它就会出现在很多个后缀中,也就是很多个后缀的公共前缀
我们已经确定了它第一次出现在第x个后缀中,以x为起点,n为终点,二分查找一个最小的ans,使得的区间[x~ans]中每个后缀的公共前缀都大于等于k
即表示sa[x]~sa[ans]这些后缀中都有第k大子串

我们还需在sa[x]~sa[ans]这些后缀中找出最小值,即第k大子串在原串中最早出现的位置就是左端点位置,然后加上len就得到右端点位置

为了实现两次区间查询,我们需要使用两个RMQ,第一个用来处理区间最小lcp,第二个用来处理区间最小sa值

关键点:
1.子串就是后缀的前缀
2.排名靠前的后缀,并不是在原串中位置靠前的后缀,这两者毫无关系

代码:

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 1e5+10;
typedef long long LL;

int s[MAXN];  // s 数组保存了字符串中的每个元素值,除最后一个元素外,每个元素的值在 1..m 之间,最后一个元素的值为 0
int wa[MAXN], wb[MAXN], wc[MAXN], wd[MAXN];  // 这 4 个数组是后缀数组计算时的临时变量,无实际意义
int sa[MAXN]; //  sa[i] 保存第 i 小的后缀在字符串中的开始下标,i 取值范围为 0..n-1
int cmp(int *r, int a, int b, int l) {
    return r[a] == r[b] && r[a + l] == r[b + l];
}
void getSA(int *r, int *sa, int n, int m) {  // n 为字符串的长度,m 为字符最大值
    int i, j, p, *x = wa, *y = wb;
    for (i = 0; i < m; ++i) wd[i] = 0;
    for (i = 0; i < n; ++i) wd[x[i] = r[i]]++;
    for (i = 1; i < m; ++i) wd[i] += wd[i - 1];
    for (i = n - 1; i >= 0; --i) sa[--wd[x[i]]] = i;
    for (j = 1, p = 1; p < n; j *= 2, m = p) {
        for (p = 0, i = n - j; i < n; ++i) y[p++] = i;
        for (i = 0; i < n; ++i) if (sa[i] >= j) y[p++] = sa[i] - j;
        for (i = 0; i < n; ++i) wc[i] = x[y[i]];
        for (i = 0; i < m; ++i) wd[i] = 0;
        for (i = 0; i < n; ++i) wd[wc[i]]++;
        for (i = 1; i < m; ++i) wd[i] += wd[i - 1];
        for (i = n - 1; i >= 0; --i) sa[--wd[wc[i]]] = y[i];
        for (swap(x, y), p = 1, x[sa[0]] = 0, i = 1; i < n; ++i)
            x[sa[i]] = cmp(y, sa[i - 1], sa[i], j) ? p - 1 : p++;
    }
    return;
}

int n;            //字符串长度
int Rank[MAXN];  // Rank[i] 表示从下标 i 开始的后缀的排名,值为 1..n
int height[MAXN]; // 下标范围为 1..n,height[1] = 0,表示suffix(sa[i-1])和suffix(sa[i])的最长公共前缀,即排名相邻的两个后缀的最长公共前缀
void getHeight(int *r,int *sa,int n) {
    int i, j, k = 0;
    for (i = 1; i <= n; ++i) Rank[sa[i]] = i;
    for (i = 0; i < n; i++) {
        if (k) k--;
        int j = sa[Rank[i] - 1];
        while (r[i + k] == r[j + k]) k++;
        height[Rank[i]] = k;
    }
    return;
}
int lcp[MAXN][30];                      //存储lcp
void init_RMQ(int n)                   //初始化rmq
{
    for(int i=1;i<n;i++) lcp[i][0]=height[i];
    for(int j=1;(1<<j)<=n;j++)
        for(int i=0;i+(1<<j)<=n;i++)
            lcp[i][j]=min(lcp[i][j-1],lcp[i+(1<<(j-1))][j-1]);
}
int RMQ(int l,int r)                 //查询l~r的lcp
{
    int k=0;
    while((1<<(k+1))<=r-l+1) k++;
    int ans=min(lcp[l][k],lcp[r-(1<<k)+1][k]);
    return ans;
}

int Min[MAXN][30];               //存储区间最小sa
void init_RMQ2(int n)
{
    for(int i=1;i<n;i++) Min[i][0]=sa[i]+1;
    for(int j=1;(1<<j)<=n;j++)
        for(int i=0;i+(1<<j)<=n;i++)
            Min[i][j]=min(Min[i][j-1],Min[i+(1<<(j-1))][j-1]);
}
int RMQ2(int l,int r)            //查询l~r的最小sa
{
    int k=0;
    while((1<<(k+1))<=r-l+1) k++;
    int ans=min(Min[l][k],Min[r-(1<<k)+1][k]);
    return ans;
}

LL sum[MAXN];
void getsum(int n)              //计算前缀和,sum[i]表示到第i个后缀为止的不同子串个数
{
    for(int i=1;i<=n;i++)
        sum[i]=sum[i-1]+(LL)(n-sa[i]-height[i]);
}
int main()
{
    string a;
    cin>>a;
    n=a.size();
    for(int i=0;i<n;i++)
        s[i]=a[i]-'a'+1;
    s[n]=0;                //必须要加!!,将s最后一位置为一个最小值

    getSA(s,sa,n+1,30);      //!!!必须是n+1!!!
    getHeight(s,sa,n);
    getsum(n);
    init_RMQ(n+1);        //注意是n+1!!!!
    init_RMQ2(n+1);


    int q;
    long long k;
    cin>>q;

    LL lp=0,rp=0;
    while(q--)
    {
        cin>>k;
        k^=lp,k^=rp,k++;
        if(k>sum[n])         //无解情况
        {
            lp=rp=0;
        }
        else
        {
            int pos=lower_bound(sum+1,sum+1+n,k)-sum;        //找到第一个大于等于k的位置,即第k大子串所在后缀的sa
            k-=sum[pos-1];           //此时k表示是当前后缀的第k个前缀
            int L=sa[pos];
            int R=L+k+height[pos]-1;
            int len=R-L+1;           //len=k+height[pos],表示第k大子串的长度
            int ll=pos+1,rr=n;       
            int ans=pos;
            while(ll<=rr)             //二分找到一个mid,使得sa[pos]~sa[mid]这几段后缀中都有一个长度大于等于len的公共前缀,即表示存在可行解
            {
                int mid=(ll+rr)>>1;
                if(RMQ(pos+1,mid)<len)    
                    rr=mid-1;
                else
                {
                    ans=mid;
                    ll=mid+1;
                }
            }
            int tmp=RMQ2(pos,ans);    //在sa[pos]~sa[ans]这几段后缀中找一个最早出现的sa,即是左端点位置
            lp=tmp;
            rp=tmp+len-1;     //左端点位置加上长度,就是右端点位置
        }
        cout<<lp<<" "<<rp<<endl;

    }
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ogmx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值