主席树的全称是可持久化权值线段树,即主席树 ⫋ 可持久化线段树。 ——某大佬博客如是云
个人理解
主席树是一棵权值线段树。其节点维护的区间表示值域,储存原数列中有多少数在该值域内。(例如,维护[L~R]的区间的节点,其储存数列中大小在[L~R]之间的数的个数)
对于数列上的每一个位置都建一棵权值线段树,再通过可持久化线段树的克隆节点的操作,在加入一个新位置上的数时,都只新建由于新加入一个数而数值发生改变的节点,从而优化时空间复杂度。
在查询时,利用每棵权值线段树结构相同的性质,通过前缀和来进行指定区间的查询。
洛谷P3834可持久化线段树2
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=2e5;
int n,m,cnt,tot;
int a[N],b[N],rt[N];
int sum[30*N],lc[30*N],rc[30*N];//储存每个节点的信息,都要开大30倍
void build(int &k,int l,int r)
{
k=++tot;//新建节点
if(l==r) return;
int mid=(l+r)>>1;
build(lc[k],l,mid);//建左子树
build(rc[k],mid+1,r);//建右子树
}
int update(int pre,int l,int r,int p)//pre指向第i-1棵线段树上维护相同区间的节点
{
int k=++tot;//新建节点
//将pre节点的信息复制到当前节点上;因为当前区间比修改前新增了一个p,所以sum+1
lc[k]=lc[pre],rc[k]=rc[pre],sum[k]=sum[pre]+1;
if(l==r) return k;//叶子节点无需继续向下拓展,直接返回
int mid=(l+r)>>1;
if(p<=mid) lc[k]=update(lc[pre],l,mid,p);//如果左儿子需要修改
else rc[k]=update(rc[pre],mid+1,r,p);//如果右儿子需要修改
return k;
}
int query(int u,int v,int l,int r,int k)
{
//设两棵线段树为i和j
//那么x表示a[i+1]~a[j]的区间内有x个数,其离散化后下标的大小在对应节点维护区间的范围内
int mid=(l+r)>>1,x=sum[lc[v]]-sum[lc[u]];
if(l==r) return l;//找到第kth的数离散化后的下标,直接返回
if(k<=x) return query(lc[u],lc[v],l,mid,k);//kth的数在左儿子的区间中
//kth的数在右儿子的区间中,因为左儿子中有x个数,kth的数在右儿子中的排名需减去x
else return query(rc[u],rc[v],mid+1,r,k-x);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1,k;i<=n;i++)
scanf("%d",&a[i]),b[i]=a[i];
sort(b+1,b+n+1);
cnt=unique(b+1,b+n+1)-b-1;
build(rt[0],1,cnt);//建一棵空树,类似前缀和的s[0]=0
for(int i=1;i<=n;i++)
{
int p=lower_bound(b+1,b+cnt+1,a[i])-b;//得到a[i]离散化之后的下标
rt[i]=update(rt[i-1],1,cnt,p);//返回第i棵线段树的根节点
}
for(int i=1,l,r,k;i<=m;i++)
{
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",b[query(rt[l-1],rt[r],1,cnt,k)]);
}
return 0;
}