主席树介绍 : 洛谷P3834 可持久化线段树 1(主席树)
昨天开始进入主席树的学习。。
首先,主席树也是线段树的一种,但是空间比线段树更大,这是因为,他记录了每一次修改之前的状态,也就是说,他比一般的线段树有更多的节点,一开始看到有题解说,每一个节点都存了一个线段树,这把我吓了一跳,其实准确来说,应该不是这样的,下面看我讲解:
主席树,是在权值线段树上发展来的,对于每一个 L == R 的节点,其保存的值是 L 存在多少个
也就是说,我们每一次更新,都是修改值的个数。那如何来修改呢,将原来版本的线段树整个copy出来是肯定不可行的,那我们来看看新的线段树和旧的线段树有哪些共同之处:
我们对每一个值的修改,也就是说,我们只需要对包含这个值的区间进行修改即可,原来不需要修改的节点,我们依然可以使用,于是就将修改过后的父亲节点连在了原来的子节点上,对于需要修改的节点,我们就新建一个节点代替它。
一颗主席树就是这样构成的
下面我推荐另一个博主的博客(因为有图啊!有图有真相!是我太懒不想画图):https://blog.youkuaiyun.com/pengwill97/article/details/80920143
接下来,我们回到这道题上来,这是一道非常经典的题目,大意是:给你一串数,然后再给你多个区间,求区间第k大的数。一道一样的题目(注意范围不同)HDU2665(http://acm.hdu.edu.cn/showproblem.pp?pid=2665)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=2e5+1,LOG=20;
int n,m,tot=0,len;
int a[maxn],b[maxn],tree[maxn],L[maxn*LOG],R[maxn*LOG],sum[maxn*LOG];//O(nlogn + mlogn)主席树的空间复杂度
int build(int l,int r){
int root=++tot;
if(l<r){
int mid=(l+r)>>1;
L[root]=build(l,mid);
R[root]=build(mid+1,r);
}
return root;
}
void discrete(){
sort(b+1,b+1+n);
len=unique(b+1,b+1+n)-b-1;
tree[0]=build(1,len);//先建一课空树(所有值为0)只有节点编号存在
}
int update(int pre,int l,int r,int x){//pre=上一个主席树的根节点,l和r为当前节点的区间,x为插入的值
int root=++tot;
L[root]=L[pre],R[root]=R[pre],sum[root]=sum[pre]+1;
//先初始化一下,L代表root节点的左节点的编号,R同理,sum代表当前节点里的数的个数(插入进来肯定要+1)
if(l<r){
int mid=(l+r)>>1;
if(x<=mid)//看插入的数所在的区间,对应区间需要新建一个节点,作为当前节点儿子,不需要的当然不用修改了
L[root]=update(L[pre],l,mid,x);
else
R[root]=update(R[pre],mid+1,r,x);
}
return root;
}
int query(int fr,int to,int l,int r,int k){
if(l==r) return l;//反正一直查到底就是答案了
int x=sum[L[to]]-sum[L[fr]];//算出这个区间的左边在下有多少个元素
int mid=(l+r)>>1;
if(k>x) return query(R[fr],R[to],mid+1,r,k-x);//看是否需要查询右儿子
else return query(L[fr],L[to],l,mid,k);
}
int main(){
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),b[i]=a[i];
discrete();//离散化
for(int i=1;i<=n;i++){
int num=lower_bound(b+1,b+1+len,a[i])-b;
tree[i]=update(tree[i-1],1,len,num);//将离散化后的数插入主席树,每一次插入都是一次更新
}
while(m--){
int le,ri,k;
scanf("%d %d %d",&le,&ri,&k);
if(le>ri) swap(le,ri);
int ans=query(tree[le-1],tree[ri],1,len,k);//有点类似于前缀和
//因为我们是按照原序列的顺序update的,所以我们只需要将两颗不同状态的线段树相减,即可得出区间序列
//然后又因为是权值线段树,所以我们只需要根据所得线段树的个数来求解即可
printf("%d\n",b[ans]);
}
return 0;
}