【POJ】【2104】区间第K大

时间:2021-08-15 15:46:34

可持久化线段树

  可持久化线段树是一种神奇的数据结构,它跟我们原来常用的线段树不同,它每次更新是不更改原来数据的,而是新开节点,维护它的历史版本,实现“可持久化”。(当然视情况也会有需要修改的时候)

  可持久化线段树的应用有很多,仅以区间第K大这种简单的问题来介绍这种数据结构。

  我们原本建立的线段树是表示区间的,或者说,维护的是【位置】,存的是每个位置上的各种信息。它的优点是满足区间加法,但不满足区间减法,所以我们这里要换一种建树方式:对于每个区间[1,i]建立一棵权值线段树。这个线段树的作用其实就跟前缀和差不多,且像前缀和一样满足区间减法!只不过我们在求前缀和的时候保留的是sum,而权值线段树把所有的值都存下来了。

  这里说一下它的保存方式:对[1,x]这个节点,它需要维护一个cnt值,表示在[1,x]这个值域,有cnt个数。

  举个栗子,我们现在有一个序列{1,2,3,4,5,2,3,3,3,3}

  然后对于表示区间[1,10]的线段树,它的节点是这样建的

      【POJ】【2104】区间第K大

  可以看出,值在[1,5]的有10个数,在[1,2]的有3个数……以此类推

  那么我们在查询第K大的时候,就可以像平衡树那样!如果左儿子的cnt>=k则在左边找,否则在右边找,那么我们就可以顺利地查询到第K大了~

  那么问题来了:如果我想查询[3,7]这个区间上第3大的数应该怎么办呢?(这个地方容易晕,一定要分清原序列的区间和值域,虽然都是用方括号的区间表示的……如果看了这句话更晕了,那就忘了它吧)

  那么就要回到我们之前说的【前缀和】上来了,我们以前快速求[l,r]的区间和,是利用前缀和[1,l-1]和[1,r]区间相减快速计算的,同理,我们也可以利用[1,l-1]和[1,r]两棵线段树来进行区间第K大的查询。即在两棵树上同时往下走!详见代码。

 //POJ 2104
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define rep(i,n) for(int i=0;i<n;++i)
#define F(i,j,n) for(int i=j;i<=n;++i)
#define D(i,j,n) for(int i=j;i>=n;--i)
using namespace std;
const int N=;
//#define debug struct node{
int x,num,rank;
}a[N];
bool cmpx(node a,node b){
return a.x<b.x;
}
bool cmpn(node a,node b){
return a.num<b.num;
} struct Tree{
int cnt,l,r;
}t[N*];
int root[N],cnt=,n,m; #define mid (l+r>>1)
void updata(int &o,int l,int r,int pos){
t[++cnt]=t[o], o=cnt, ++t[o].cnt;
if (l==r) return;
if (pos<=mid) updata(t[o].l,l,mid,pos);
else updata(t[o].r,mid+,r,pos);
#ifdef debug
printf("%d %d %d %d\n",o,l,r,pos);
#endif
} int query(int i,int j,int rank){
i=root[i],j=root[j];
int l=,r=n;
while(l!=r){
if (t[t[j].l].cnt-t[t[i].l].cnt>=rank)//在两棵树上一起往下走
r=mid,i=t[i].l,j=t[j].l;
else{
rank-=t[t[j].l].cnt-t[t[i].l].cnt;
l=mid+,i=t[i].r,j=t[j].r;
}
}
return l;
}
#undef mid int main(){
freopen("file.in","r",stdin);
scanf("%d%d",&n,&m);
int x=;
F(i,,n) {scanf("%d",&a[i].x); a[i].num=i;}
sort(a+,a+n+,cmpx);
F(i,,n) a[i].rank=i;
sort(a+,a+n+,cmpn);
F(i,,n) {
root[i]=root[i-];
updata(root[i],,n,a[i].rank);//此处可以先不理解……
//简单来说就是:为了节约空间,我们并不需要真的给每个区间建一棵完整的线段树
//而是可以在原来的基础上进行新的维护(即原来的为历史版本)
}
sort(a+,a+n+,cmpx);
F(i,,m){
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",a[query(l-,r,k)].x);
}
return ;
}