zoukankan      html  css  js  c++  java
  • 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;
    }
  • 相关阅读:
    解析XML
    事务
    js小工具
    plsql用过的流程语句
    查询语句
    存储过程
    用过的CRT命令
    mysql常用命令
    Spirng MVC demo 完整示例01 环境搭建
    jmeter多个http请求串联
  • 原文地址:https://www.cnblogs.com/6262369sss/p/11439511.html
Copyright © 2011-2022 走看看