主席树(可持续化线段树)

我们知道,线段树可以在 O(logn) 时间内完成区间(单点)更新,区间求极值与区间求和。
但是,对于下面这个问题:

给定一个长度为 n 的序列,有m个询问,对于每个询问,输出区间 [L,R] 中的第 K 大的值。(n,m<=100000)

对于这个问题 ,似乎单纯的线段树并不能解决。
先让我们来考虑一个简单的问题:

给定一个区间,求出这个区间中的第 K 大的值。

当然,对于这个问题,直接排序是最简单的做法。但我们同样可以用线段树解决:保存一颗权值线段树,线段树节点上存的即是该节点表示的权值范围内数的个数。然后从根开始,根据左右子树的cnt值判断下一步递归在左子树还是右子树,到叶子时,就求到了答案。
根据这个思想,我们希望对于询问的每一个区间,都有一颗这样的线段树,因此引入前缀和的思想:依次将数组中的值添加进线段树,保存 n 棵这样的线段树,即前缀。因为这样的线段树是可以相减的,减完就得到了我们想要的线段树,就可以用O(logn)时间求得结果了。但是这样需要的空间为 n2 ,是肯定开不下的。
更新
但是,如图,当我们向其中插入一个1时,需要更新的点只有红色线上的3个点。
我们可以发现当我们更新一个点时,实际上是只有 logn 个点需要被更新,而其他的点可以继承原来的状态。
因此,我们需要的空间只是 nlogn 的大小。具体: (2n+nlogn)
于是,主席树就建立完毕了。
注:
因为是权值线段树,所以只能离线处理,以及数据需要离散。
保存一个sum值即可求出前k大值的和,详见IOI2014Holiday题解

#include<stdio.h>
#include<iostream>
#include<algorithm>
#define M 100005
#define MLOGM 2000005
using namespace std;
int Lson[MLOGM],Rson[MLOGM],cnt[MLOGM],A[M],B[M],rt[MLOGM];
int n,m,tot=0;
void build(int L,int R,int &tid){
    tid=++tot;
    cnt[tid]=0;
    if(L==R)return;
    int mid=(L+R)>>1;
    build(L,mid,Lson[tid]);
    build(mid+1,R,Rson[tid]);
}
void insert(int od,int &tid,int L,int R,int x){
    tid=++tot;
    Lson[tid]=Lson[od];
    Rson[tid]=Rson[od];
    cnt[tid]=cnt[od]+1;
    if(L==R)return;
    int mid=(L+R)>>1;
    if(x<=mid)insert(Lson[od],Lson[tid],L,mid,x);
    else insert(Rson[od],Rson[tid],mid+1,R,x);
}
int query(int od,int id,int L,int R,int k){
    if(L==R)return B[L];
    int res=cnt[Rson[id]]-cnt[Rson[od]];
    int mid=(L+R)>>1;
    if(res>=k)return query(Rson[od],Rson[id],mid+1,R,k);
    else return query(Lson[od],Lson[id],L,mid,k-res);
}
int main(){
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&A[i]);
        B[i-1]=A[i];
    }
    sort(B,B+n);
    int n1=unique(B,B+n)-B;
    build(0,n1-1,rt[0]);
    for(int i=1;i<=n;i++){
        A[i]=lower_bound(B,B+n1,A[i])-B;
        insert(rt[i-1],rt[i],0,n1-1,A[i]);
    }
    for(int i=1;i<=m;i++){
        int l,r,k;
        scanf("%d %d %d",&l,&r,&k);
        printf("%d\n",query(rt[l-1],rt[r],0,n1-1,k));
    }
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值