区间第K大值

数据结构专题

做法有多种:
1、二分+分块
2、二分+归并树
3、划分树
4、主席树

其中第3种我没有写。
这四种方法各自的时间空间复杂度都不一样,推广性也不一样。

时间:
二分+分块:O(nlog√(nlogn)+m√nlog1.5n)
二分+归并树:O(nlogn+mlog3n)
主席树:O( (n+m)logn )

推广性:
二分+分块:推广性强,效率不高
二分+归并树:推广性弱,效率较高
主席树:推广性极强,效率极高

现在分别讲解这三种方法

一、二分+分块:

分两块:二分和分块(说和没说一样)

我们发现一个对于一个数x,x越大,那么他的排名就越靠前,那么就是线性的,可以用二分。

二分后就要判断其是第几大数。如果一个一个判断过来,不如直接写。可以用分块。把整个分成几块后,将每一块里的数都进行排序。排序有什么好处?可以用二分查找(lower_ bound或upper_ bound,具体看题目)。这样就很高效了。别忘了一些不在完整块里的残留要循环过去。

还有一点是要离散,数据很大,不离散内存会炸。

Code:

#include<bits/stdc++.h>
using namespace std;
int A[30005],B[30005],n,cas,N,C[30005];
bool check(int l,int r,int x,int P){//分块求值
    int L=l/N,R=r/N,res=0;
    for(int i=L+1;i<R;i++){//完整块内
        int sum=lower_bound(A+i*N+1,A+(i+1)*N+1,x)-A-1-i*N;
        res+=sum;
    }
    if(L!=R){//块外
        for(int i=l;i<=(L+1)*N;i++)if(C[i]<x)res++;
        for(int i=R*N+1;i<=r;i++)if(C[i]<x)res++;
    }else{
        for(int i=l;i<=r;i++)if(C[i]<x)res++;
    }
    res=r-l+1-res;
    if(res>=P)return 1;
    return 0;
}
int main(){
    scanf("%d%d",&n,&cas);
    for(int i=1;i<=n;i++)scanf("%d",&A[i]),B[i]=A[i],C[i]=A[i];
    sort(B+1,B+1+n);
    N=sqrt(n);//这样的N其实不是最优的,最优的是N=sqrt(n*log2(n)),可以手推一下在这道题里这样最快
    for(int i=0;i<N;i++)sort(A+N*i+1,A+N*(i+1)+1);//每一块排序
    sort(A+N*N+1,A+1+n);//最后一块不完整,单独排序
    while(cas--){
        int l,r,k;
        scanf("%d%d%d",&l,&r,&k);
        int L=1,R=n,ans;
        while(L<=R){//二分枚举一个x
            int mid=(L+R)/2;
            if(check(l,r,B[mid],k))L=mid+1,ans=mid;
            else R=mid-1;
        }
        printf("%d\n",B[ans]);//找到的只是离散后的下标
    }
    return 0;
}

二、二分+归并树

二分和上面的是一样的。

归并树是新学的。个人的理解是把归并排序时这一层的状态全部记录下来,而不是全部排完。这样就可以按照线段树的查询方式往下递归,然后再用二分查找求答案。

Code:

#include<bits/stdc++.h>
#define M 30005
using namespace std;
int A[M],S[20][M],B[M];
struct node{int L,R;}tree[M*4];
void Build(int L,int R,int p,int dep){//建树
    tree[p].L=L;tree[p].R=R;
    if(L==R){
        S[dep][L]=A[L];
        return;
    }
    int mid=(L+R)/2;
    Build(L,mid,p*2,dep+1);Build(mid+1,R,p*2+1,dep+1);//先把后面的算出来,和归并排序时一样的
    int i=L,j=mid+1,k=L;
    while(i<=mid&&j<=R)
        if(S[dep+1][i]<S[dep+1][j])S[dep][k++]=S[dep+1][i++];
        else S[dep][k++]=S[dep+1][j++];
    while(i<=mid)S[dep][k++]=S[dep+1][i++];
    while(j<=R)S[dep][k++]=S[dep+1][j++];
}
int Query(int L,int R,int p,int dep,int x){//和线段树查询基本一样
    if(tree[p].L==L&&tree[p].R==R){
        return lower_bound(S[dep]+L,S[dep]+R+1,x)-S[dep]-L;
    }
    int mid=(tree[p].L+tree[p].R)/2;
    if(mid>=R)return Query(L,R,p*2,dep+1,x);
    else if(mid<L)return Query(L,R,p*2+1,dep+1,x);
    else return Query(L,mid,p*2,dep+1,x)+Query(mid+1,R,p*2+1,dep+1,x);
}
int main(){
    int n,cas;
    scanf("%d%d",&n,&cas);
    for(int i=1;i<=n;i++)scanf("%d",&A[i]),B[i]=A[i];
    sort(B+1,B+1+n);
    Build(1,n,1,1);
    while(cas--){//此段与上一个同类
        int l,r,x;
        scanf("%d%d%d",&l,&r,&x);
        int L=1,R=n,ans=0;
        while(L<=R){
            int mid=(L+R)/2;
            int res=r-l+1-Query(l,r,1,1,B[mid]);
            if(res>=x)L=mid+1,ans=mid;
            else R=mid-1;
        }
        printf("%d\n",B[ans]);
    }
    return 0;
}

三、主席树

这个才是本篇的重点。虽然我依旧不是很会主席树,但是手推的话大致也是懂得了那么一点点。主席树就是一种权值前缀和,但是如果把每个前缀和造成一棵树,那么内存就太耗了。如果把数全部画出来后,就可以发现其实有很多都是重复的。那么如何利用这些重复的点?很容易想到的就是共用这些点。那么如何实现?其实我也不是很会……(读者还是上网自己查吧,等以后我再补)

这道题的主席树是静态的,即不能更新(插入不算更新),反正这道题只有查询。

Code:

    #include<bits/stdc++.h>
#define M 30005
using namespace std;
int A[M],B[M],Lt[M*20],Rt[M*20],Gt[M*20],Sum[M*20],tot;//tot是节点的编号
void Build(int L,int R,int &tid){//造树
    tid=++tot;Sum[tid]=0;
    if(L==R)return;
    int mid=(L+R)/2;
    Build(L,mid,Lt[tid]);Build(mid+1,R,Rt[tid]);
}
void Insert(int lat,int L,int R,int x,int &tid){//插入一个新数
    tid=++tot;
    Lt[tid]=Lt[lat];Rt[tid]=Rt[lat];//这些大概是就是把不共用的造出来,公用的不变
    Sum[tid]=Sum[lat]+1;
    if(L==R)return;
    int mid=(L+R)/2;
    if(mid>=x)Insert(Lt[lat],L,mid,x,Lt[tid]);
    else Insert(Rt[lat],mid+1,R,x,Rt[tid]);
}
int Query(int lt,int rt,int L,int R,int x){
    if(L==R)return L;
    int cnt=Sum[Lt[rt]]-Sum[Lt[lt]];
    int mid=(L+R)/2;
    if(x<=cnt)return Query(Lt[lt],Lt[rt],L,mid,x);
    else return Query(Rt[lt],Rt[rt],mid+1,R,x-cnt);
}
int main(){
    int n,cas;
    scanf("%d%d",&n,&cas);
    for(int i=1;i<=n;i++)scanf("%d",&A[i]),B[i]=A[i];
    sort(B+1,B+1+n);int m=unique(B+1,B+1+n)-B-1;//离线去重都是必须的
    for(int i=1;i<=n;i++)A[i]=lower_bound(B+1,B+1+m,A[i])-B;
    Build(1,m,Gt[0]);//现造一棵空树
    for(int i=1;i<=n;i++)Insert(Gt[i-1],1,m,A[i],Gt[i]);//每次插入一个点
    while(cas--){
        int l,r,x;
        scanf("%d%d%d",&l,&r,&x);
        printf("%d\n",B[Query(Gt[l-1],Gt[r],1,m,r-l+2-x)]);
    }
    return 0;
}

这是数据结构专题的第一篇,之后还有其他的,都是一些数据结构来优化程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值