可持久化线段树
原理
什么是可持久化线段树呢,为我们知道对于一个区间[l,r]我们可以用线段树去维护这个区间最值,并且可以用权值线段树去维护区间[1,r]的第k大,but对于任意区间的第k大该如何去求呢,其实思想也很简单,我们只要开n个权值线段树,之后利用前缀和的思想就可以求出来了,但是开n个线段树这个空间复杂度实在太大,所以我们hjt大佬就开发出了主席树。
图解
例如,一列数,n为6,数分别为1 3 2 3 6 1
首先,每棵树都是这样的:
以第4棵线段树为例,1~4的数分别为1 3 2 3
因为是同一个问题,n棵权值线段树的形状是一模一样的,只有节点的权值不一样
所以这样的两棵线段树之间是可以相加减的(两颗线段树相减就是每个节点对应相减)。
现在我们对前缀和的思想解释一下
想想,第x棵线段树减去第y棵线段树会发生什么?
第x棵线段树代表的区间是[1,x]
第y棵线段树代表的区间是[1,y]
两棵线段树一减
设x>y,[ 1 , x ] − [ 1 , y ] = [ y + 1 , x ] [1,x]-[1,y]=[y+1,x][1,x]−[1,y]=[y+1,x]
所以这两棵线段树相减可以产生一个新的区间对应的线段树!
等等,这不是前缀和的思想吗
这样一来,任意一个区间的线段树,都可以由我这n个基础区间表示出来了!
因为每个区间都有一个线段树
然后询问对应区间,在区间对应的线段树中查找kth就行了
假设现在有一棵线段树,序列往右移一位,建一棵新的线段树
对于一个儿子的值域区间,如果权值有变化,那么新建一个节点,否则,连到原来的那个节点上
现在举几个例子来说明
序列4 3 2 3 6 1
区间[1,1]的线段树(蓝色节点为新节点)
区间[1,2]的线段树(橙色节点为新节点)
区间[1,3]的线段树(紫色节点为新节点)
这样是不是非常优秀啊?
主席树的思想就讲到这里,接下来具体的代码来实现它
代码
a、b数组,一般储存输入数据
sz:节点个数
tr数组:存储每棵线段树的根节点编号
lc、rc数组:记录左儿子、右儿子编号,类似于动态开点
sum数组:记录节点权值
q:记录离散化后序列长度,也是线段树的区间最大长度
建空树
void build(int l,int r,int &tr){
tr=sz++;
sum[tr]=0;
if(l==r)return;
int mid=(l+r)>>1;
build(l,mid,lc[tr]);
build(mid+1,r,rc[tr]);
}
离散化
sort(b+1,b+1+n);
int q=unique(b+1,b+1+n)-b-1;
加点
int update(int l,int r,int o,int c){//c代表加入的这个点是老几
int oo=sz++;
lc[oo]=lc[o];
rc[oo]=rc[o];
sum[oo]=sum[o]+1;
if(l==r)return oo;
int mid=(l+r)>>1;
if(mid>=c) lc[oo]=update(l,mid,lc[oo],c);
else rc[oo]=update(mid+1,r,rc[oo],c);
return oo;
}
求第k大
int query(int l,int r,int u,int v,int k){
int mid=(l+r)>>1;
if(l==r) return l;
int g=sum[lc[v]]-sum[lc[u]];
if(g>=k) return query(l,mid,lc[u],lc[v],k);
else return query(mid+1,r,rc[u],rc[v],k-g);//注意这的写法
}
int main(){
......
scanf("%d%d%d"&l,&r,&k);
int ans=b[query(1,q,tr[l-1],tr[r],k)];
}
完整代码
#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
//主席树一般开32倍空间
int n,m;//节点个数和查询个数
int a[N];//原数组
int b[N];//离散化数组
int q;//离散化后的长度
int zs=0;//节点个数,也是给每一个新节点分配编号的
int sum[N<<5];//存权值的
int tr[N<<5];//存每一个线段树的根节点的标号
int rc[N<<5];//左儿子
int lc[N<<5];//右儿子
void build(int l,int r,int &tr){
tr=++zs;
sum[zs]=0;//新点
if(l==r) {
return ;
}
int mid=(l+r)>>1;
build(l,mid,lc[tr]);
build(mid+1,r,rc[tr]);
}
int update(int o,int l,int r,int p){
int oo=++zs;//新点
lc[oo]=lc[o];
rc[oo]=rc[o];
sum[oo]=sum[o]+1;
if(l==r) return oo;
int mid=(l+r)>>1;
if(mid>=p) lc[oo]=update(lc[oo],l,mid,p);//要找的那个点在哪就往哪里跑,同时 这个赋值操作其实就相当于多开了一条链
else rc[oo]=update(rc[oo],mid+1,r,p);
return oo;
}
int query(int l,int r,int u,int v,int k){//v,u的区间的第k大
//这里我们用前缀和思想,[1,v]-[1,u-1]=[u,v] 注意这里我是在主函数里进行的操作
int mid=(l+r)>>1;
if(l==r) return l;//找到了就返回
int x=sum[lc[v]]-sum[lc[u]];
if(x>=k) return query(l,mid,lc[u],lc[v],k);
else return query(mid+1,r,rc[u],rc[v],k-x);//注意这里 要去找右儿子的第k-x大
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(b+1,b+1+n);
q=unique(b+1,b+1+n)-b-1;//离散化
build(1,q,tr[0]);//建一棵空树,主要是为了不出错 ,空树看成第0棵树
int c;
for(int i=1;i<=n;i++){
c=lower_bound(b+1,b+1+q,a[i])-b;
tr[i]=update(tr[i-1],1,q,c);//依次建树
}
int l,r,k;
for(int i=1;i<=m;i++){
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",b[ query(1,q,tr[l-1],tr[r],k) ]);
}
return 0;
}
小结
这还一个比较基础的数据结构,在下来应该就是树套树,或者是平衡树,总而言之我还是会朝着2022noi上海华二努力的~
有部分转载于https://blog.csdn.net/ModestCoder_/article/details/90107874?