我们知道,线段树可以在
O(logn)
时间内完成区间(单点)更新,区间求极值与区间求和。
但是,对于下面这个问题:
给定一个长度为 n 的序列,有
m 个询问,对于每个询问,输出区间 [L,R] 中的第 K 大的值。(n,m<=100000)
对于这个问题 ,似乎单纯的线段树并不能解决。
先让我们来考虑一个简单的问题:
给定一个区间,求出这个区间中的第 K 大的值。
当然,对于这个问题,直接排序是最简单的做法。但我们同样可以用线段树解决:保存一颗权值线段树,线段树节点上存的即是该节点表示的权值范围内数的个数。然后从根开始,根据左右子树的
根据这个思想,我们希望对于询问的每一个区间,都有一颗这样的线段树,因此引入前缀和的思想:依次将数组中的值添加进线段树,保存
n
棵这样的线段树,即前缀。因为这样的线段树是可以相减的,减完就得到了我们想要的线段树,就可以用
但是,如图,当我们向其中插入一个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;
}