主席树

可持久化线段树

原理

什么是可持久化线段树呢,为我们知道对于一个区间[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?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值