hdu6704 后缀数组+主席树+ST +二分

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6704

推荐博客:https://www.cnblogs.com/1625--H/p/11403199.html

题意:输入t,接下来t组数据,每组数据输入n和q,n代表字符串长度,q代表查询次数,接下来一行输入长度为n的字符串,最后再输入q行查询,每一个查询有l,r,k,查询字符串中在区间[l,r]里的这个子串第k次出现的位置,位置就是出现这个子串的首字母位置,如果没有第k次,则输出-1。

比赛时我也没写出来,补题。

思路:我讲的可能不是很清楚,可以看上面的推荐博客。

区间[l,r]的子串在位置为l的后缀中是一定出现了的,并且是这个后缀的一个前缀,而要找到其他出现这个子串的位置,我们可以用后缀数组,先用后缀数组先把原串所有的后缀进行排序,即求出sa数组,其中出现这个子串的后缀肯定都是连续的,因为它都是这些连续后缀的前缀,这里我们可以先求出后缀数组中的height[i]数组,i是排位,height[i]代表排位为i的后缀和排位为i-1的后缀之间的最长公共前缀(LCP),我们以位置为l的后缀的排位x[l]为中心,向左右两边分别找最后一个和位置为l的后缀之间LCP>=r-l+1的后缀,得到两个后缀的排位(假设分别是L和R),那么在排位区间[L,R]中出现的所有后缀的前缀都包含着我们要查询的子串,这个过程可以用二分来实现。我们要找其中出现的第k个,实际上就只需要寻找这个排位区间中的位置第k大,这个我们可以用静态主席树做到。

代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
#include<map>
#include<stack>
#include<cmath>
#include<vector>
#include<set>
#include<cstdio>
#include<string>
#include<deque> 
using namespace std;
typedef long long LL;
#define eps 1e-8
#define INF 0x3f3f3f3f
#define maxn 100005
int sa[maxn],height[maxn],x[maxn],y[maxn],c[maxn];
//这里x数组对应于rank数组,但是我个人习惯写x了 
int n,m,k,t,q,cnt;
char s[maxn]; 
int dp[maxn][20],root[maxn];
struct node{//静态主席树也是用结构体写的 
    int l,r,sum;
}tree[maxn<<6];
void getHeight(){//height数字下标表示排位,值表示排位为i的后缀和排位为i-1的后缀的LCP
                //H数组下标表示位置,H[i]=height[x[i]],有H[i]>=H[i-1]-1 
    int k=0; 
    for(int i=1;i<=n;i++) x[sa[i]]=i;
    for(int i=1;i<=n;i++){
        if(k) k--;
        int j=sa[x[i]-1];//找到位置在i的关键字的前一个排位的关键字 
        while(s[i+k]==s[j+k]&&(i+k)<=n&&(j+k)<=n) k++;
        height[x[i]]=k;
    } 
}
void build_sa(){//好久没写这个了,所以写的时候加了很多注释,就当梳理思路,复习了 
    m=256;
    for(int i=1;i<=m;i++) c[i]=0;
    for(int i=1;i<=n;i++) c[x[i]=s[i]]++;//x数组下标表示位置,值表示排位 
    for(int i=1;i<=m;i++) c[i]+=c[i-1];
    for(int i=n;i>=1;i--) sa[c[x[i]]--]=i;//sa数组下标表示排位,值表示第一关键字的位置 
    for(int k=1;k<n;k<<=1){
        int num=0;
        for(int i=n-k+1;i<=n;i++) y[++num]=i;//y数组下标表示第二关键字排位,值表示对应的第一关键字的位置
        // 有部分第一关键字没有第二关键字,这里把它们的第二关键字补'0',按照出现顺序排序 
        for(int i=1;i<=n;i++){//按照关键字排位从小到大来遍历第一关键字 
            if(sa[i]>k)//排位为i的第一关键字位置如果大于k,则说明这个关键字可以作为另一个关键字的第二关键字 
            y[++num]=sa[i]-k;// 这个关键字作为第二关键字的话,它所对应的第一关键字的位置就是sa[i]-k 
        }
        
        //上面已经求出了排位为i的第一关键字和第二关键字的对应的位置,下面开始对第一关键字和对应的第二关键字进行合并
        //并求出合并之后的关键字对应的sa数组,下标表示排位,值表示位置 
        for(int i=1;i<=m;i++) c[i]=0;
        for(int i=1;i<=n;i++) c[x[i]]++;
        for(int i=1;i<=m;i++) c[i]+=c[i-1];
        //我们这里i从大到小,在第一关键字不同时,这里的y[i]没有什么影响,但是在第一关键字相同时,第二关键字
        //更大的关键字会先出现,从而获得更大的位置,这就保证了先比较第一关键字,相同时比较第二关键字 
        for(int i=n;i>=1;i--) sa[c[x[y[i]]]--]=y[i];
        
        swap(x,y);//接下来我们要求出合并之后关键字的x数组,下标表示位置,值表示排位,所以我们把x数组的值复制到y数组 
        num=1;
        x[sa[1]]=1; 
        for(int i=2;i<=n;i++)//按照排位从小到大的顺序遍历y数组 
        x[sa[i]]=y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?num:++num;
        m=num;
        if(m>=n) break; 
    }
}
void ST(){
    for(int i=1;i<=n;i++)
    dp[i][0]=height[i];//i代表的是排位 
    for(int j=1;(1<<j)<=n;j++){
        for(int i=1;i+(1<<j)-1<=n;i++){
            int a=dp[i][j-1];
            int b=dp[i+(1<<(j-1))][j-1];
            dp[i][j]=min(a,b);
        }
    }
}
void init(){
    memset(root,0,sizeof(root));
    memset(height,0,sizeof(height)); 
    memset(dp,0,sizeof(dp));
    cnt=0;
}
void build_0(int &root,int l,int r){
    root=++cnt;         
    tree[root].sum=0;//在这个位置卡了一天,把他写在下面了,并且这个函数中没有向上更新 
    if(l==r){
        return;
    }
    int mid=(l+r)/2;
    build_0(tree[root].l,l,mid);
    build_0(tree[root].r,mid+1,r);
}
void update(int root){
    tree[root].sum=tree[tree[root].l].sum+tree[tree[root].r].sum;
}
void build(int pre,int &root,int l,int r,int index){
    root=++cnt;
    tree[root]=tree[pre];
    if(l==r){
        tree[root].sum=1;
        return;
    }
    int mid=(l+r)/2;
    if(index<=mid)
    build(tree[pre].l,tree[root].l,l,mid,index);
    else
    build(tree[pre].r,tree[root].r,mid+1,r,index);
    update(root);
}
int RMQ(int l,int r){//这里写的有点麻烦,这里的l和r已经是后缀的排位了 
    if(l>r) swap(l,r);
    if(l==r) return n-sa[l]+1;//l==r一定满足条件,因为就是位置为l的后缀,这里返回INF也是可以的 
    l++;//这里需要l++,因为考虑到height[i]的定义
        //当我们要查询排位在区间[l,r]之间的后缀,它们两两相邻的LCP最小值,l需要++,
        //因为height[l]是排位为l的后缀和排位为l-1的后缀的LCP
        //这里我们是不需要height[l]的,只需要height[l+1]到height[r] 
    int k=0;
    while((1<<(k+1))<=r-l+1) k++;
    int a=dp[l][k];
    int b=dp[r-(1<<k)+1][k];
    return min(a,b);
}
int find_L(int l,int r,int len){
    int R=r;
    int pre=r;//其实还是不怎么会二分的,写成这个方式比较保险 
    while(l<=r){
        int mid=(l+r)/2;
        if(RMQ(mid,R)>=len){
            pre=mid;
            r=mid-1;
        }
        else
        l=mid+1;
    }
    return pre;
}
int find_R(int l,int r,int len){
    int L=l;
    int pre=l;
    while(l<=r){
        int mid=(l+r)/2;
        if(RMQ(L,mid)>=len){
            pre=mid;
            l=mid+1;
        }
        else
        r=mid-1; 
    }
    return pre;
}
int ask(int pre,int root,int l,int r,int k){
    if(tree[root].sum-tree[pre].sum<k) return -1;
    if(l==r){
        return l;
    }
    int mid=(l+r)/2;
    if(tree[tree[root].l].sum-tree[tree[pre].l].sum>=k)
    return ask(tree[pre].l,tree[root].l,l,mid,k);
    else
    return ask(tree[pre].r,tree[root].r,mid+1,r,k-(tree[tree[root].l].sum-tree[tree[pre].l].sum));
}
int main()
{
    scanf("%d",&t);
    while(t--){
        scanf("%d%d",&n,&q);
        init();
        scanf("%s",s+1);
        build_sa();
        getHeight();
        build_0(root[0],1,n);//第0个版本的线段树,我这里直接写build_0了,个人习惯了 
        for(int i=1;i<=n;i++)//其他版本的线段树 
        build(root[i-1],root[i],1,n,sa[i]);
        ST();
        int l,r;
        while(q--){
            scanf("%d%d%d",&l,&r,&k);
            int len=r-l+1;//相邻p排位两个后缀的LCP最小要是len 
            int L=find_L(1,x[l],len);//二分找左边界 
            int R=find_R(x[l],n,len);//二分找右边界 
            LL ans;
            if(R-L+1<k) ans=-1;
            else ans=ask(root[L-1],root[R],1,n,k);//两个历史版本的线段树中查询第k大的位置 
            printf("%lld
",ans);      
        }
    }
    return 0;
}
原文地址:https://www.cnblogs.com/6262369sss/p/11439511.html