主席树详解(基础)

本文详细介绍了可持久化线段树(又称主席树)的概念,解释了其可持久化的原因及实现方式,并通过一个实例展示了如何建立和操作主席树。此外,文章还探讨了权值线段树的原理,以及如何利用可持久化线段树解决区间K小值问题,包括离散化、建立和查询的步骤。最后,文章提供了一个完整的代码示例,展示如何结合两者解决复杂问题。
摘要由CSDN通过智能技术生成

可持久化线段树,又名主席树(它的发明者叫黄嘉泰,拼音首字母为hjt,与当时主席的拼音首字母相同)。

主席树简介

什么是可持久化

为什么说可持久化线段树可持久,是因为它不同于普通的线段树,普通的线段树在修改改过后就不能保存原有的数据,而主席树可以保存以往的数据,也就是说,你可以实时回溯查找各个版本的线段树中的内容,而他是基于线段树的。

怎么可持久化

最简单的方法就是每修改一次新建一个线段树(妥妥MLE)
所以说我们需要更省空间的方法,我们先列举一个比较简单的线段树

括号表示数值,未被括起来的部分表示区间,我们注意到,如果修改第二个数,只有一条链会受到影响(2,1-2,1-4)也就是说,我们只要把这一段链存储下来就可以实现回溯(把2位置上的1改成2)

如此便实现了主席树最基本的操作

怎么写主席树

例题:可持久化线段树1
题目大意:你需要维护这样的一个长度为 N 的数组,支持如下两种操作

  1. 在某个历史版本上修改某一个位置上的值
  2. 访问某个历史版本上的某一位置的值
    也就是我们需要建立一个主席树,并且做到单点修改和单点查询,不过这道题对线段树本身要求很低

注意要点

主席树和普通的线段树有很大的区别:

  • 由于并不知道需要建多少个点,需要动态开点,也就是用一个点开一个点。
  • 建树的过程中我们并不需要实际进行什么操作
  • 一个儿子可能有多个父亲,但一个父亲只有两个儿子
  • 树会有多个根
  • 儿子的下标需要保存而不会再是简单的乘 2 ( + 1 ) 2(+1) 2(+1)
  • 函数要开 i n t int int

存树

需要 s t r u c t struct struct 保存左右儿子和值

struct{
	int l,r,v;
}t[N*50];

建树

与普通线段树建树相似,只不过儿子的下标需要保存( n u m num num 目前已经开点的个数),下标没有规律。

int build(int k,int ul,int ur){
   num++;
   k=num;//开点
   if(ul==ur){
   	t[k].v=a[ul];
   	return k;
   }	
   int mid=(ul+ur)>>1;
   t[k].l=build(t[k].l,ul,mid);
   t[k].r=build(t[k].l,mid+1,ur);
   return k;
}

单点更改

对于每一个更改,相当于新增一条链,所以说仍然需要开点,剩下的与线段树区别不大

int update(int k,int ul,int ur,int p,int change){
	num++;
	t[num]=t[k];
	k=num;//开点
	if(ul==ur){
		t[k].v=change;
	}
	else{
		int mid=(ul+ur)>>1;
		if(p<=mid)t[k].l=update(t[k].l,ul,mid,p,change);
		else t[k].r=update(t[k].r,mid+1,ur,p,change);			
	}
	return k;
}

单点查询

与线段树单点查询相同

int query(int k,int ul,int ur,int p){
	if(ul==ur){
		return t[k].v;
	}
	int mid=(ul+ur)>>1;
	if(p<=mid) return query(t[k].l,ul,mid,p);
	else return query(t[k].r,mid+1,ur,p);
}

版本保存

主席树可以回溯到各个版本的线段树,而我们如何将一个版本的线段树保存下来呢。
我们可以注意到,无论修改哪一个的点,有一个点一定会改变————根
所以说我们需要把每一个线段树根的坐标保存下来,这样子,版本回溯就相当于回溯到相应版本的根

主函数

r o o t root root 即为保存版本的根的下标的数组

int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	root[0]=build(0,1,n);
	for(int i=1;i<=m;i++){
		int vl,type,w;
		scanf("%d%d%d",&vl,&type,&w);
		if(type==1){
			int into;
			scanf("%d",&into);
			root[i]=update(root[vl],1,n,w,into);
		}
		else{
			printf("%d\n",query(root[vl],1,n,w));
			root[i]=root[vl];
		}
	}
	return 0;
}

完整代码

#include<stdio.h>
#include<algorithm>
#define N 1000005
using namespace std;
struct{
	int l,r,v;
}t[N*50];
int a[N],root[N];
int num=0;
int build(int k,int ul,int ur){
	num++;
	k=num;
	if(ul==ur){
		t[k].v=a[ul];
		return k;
	}	
	int mid=(ul+ur)>>1;
	t[k].l=build(t[k].l,ul,mid);
	t[k].r=build(t[k].l,mid+1,ur);
	return k;
}
int update(int k,int ul,int ur,int p,int change){
	num++;
	t[num]=t[k];
	k=num;
	if(ul==ur){
		t[k].v=change;
	}
	else{
		int mid=(ul+ur)>>1;
		if(p<=mid)t[k].l=update(t[k].l,ul,mid,p,change);
		else t[k].r=update(t[k].r,mid+1,ur,p,change);			
	}
	return k;
}
int query(int k,int ul,int ur,int p){
	if(ul==ur){
		return t[k].v;
	}
	int mid=(ul+ur)>>1;
	if(p<=mid) return query(t[k].l,ul,mid,p);
	else return query(t[k].r,mid+1,ur,p);
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	root[0]=build(0,1,n);
	for(int i=1;i<=m;i++){
		int vl,type,w;
		scanf("%d%d%d",&vl,&type,&w);
		if(type==1){
			int into;
			scanf("%d",&into);
			root[i]=update(root[vl],1,n,w,into);
		}
		else{
			printf("%d\n",query(root[vl],1,n,w));
			root[i]=root[vl];
		}
	}
	return 0;
}

以上便是最基础的可持续线段树
我们知道可以利用权值线段树求解第K大或第K小值,那我们可否可以使用可持续的权值线段树求解区间K大值呢?
可行!

可持续的权值线段树

前置知识(如果您已熟知,可以跳过)

权值线段树

权值线段树是维护值域的线段树,维护的是值在某个区间内数的个数。
比如说 2,1,4,3的数组
我们就可以有这样一个权值线段树:

也就是说,我们可以知道在这个数组中每一个数出现的次数,单点修改复杂度为 O ( l o g n ) O(logn) O(logn)
而它更厉害的一个用处就是——求取第K小的数。
在访问根的时候,由于树由左至右具有单调递增性,我们只需要看它的左儿子的值是否比K大即可,如果比K大,则说明K小数在做儿子所维护的区间内,反之则在右儿子所维护的区间内。
但是很明显,我们并不能用它来求解区间K小值,因为它只维护值域,所以说我们不能一一对应原数组的区间,所以说我们需要主席树的辅助来求取区间K小值

离散化

离散化讲解

因为权值线段树维护值域,而对于这样一个数列:5,1, 1 × 1 0 10 1 \times 10^{10} 1×1010,5 很明显,我们无法开出如此巨大的数组,是不是用不了权值线段树呢?
我们想到了一个技巧——哈希
也就是说,我们可以用一个较小的数表示较大的数,这样数组就可以开下了,而很明显,我们必须需要保持这几个数的大小关系不变
于是我们就产生了离散化的想法
也就是说我们先将这个序列按照大小顺序排列即 1,5,5, 1 × 1 0 10 1 \times 10^{10} 1×1010 ,此时我们在分别将他们按大小顺序替换掉(由于5等于5,所以说两者必须用大小相同的数代替)即 1,2,2,3 这个数列只代表它的大小关系,并不代表真实数值,而我们此时还需要一个哈希数组来存储对应值。

代码实现

介绍两个 S T L STL STL 函数

  • sort(开头地址,结尾地址)–排序
  • unique(开头地址,结尾地址)–去重
  • lower_bound(开头地址,结尾地址,值)–二分求解第一个大于等于“值”的数的地址
    当然 s o r t sort sort u n i q u e unique unique 也可以用 s e t set set 实现。
    代码如下:
for(int i=1;i<=n;i++){
  scanf("%d",&a[i]);
  b[i]=a[i];
}
sort(b+1,b+n+1);
int cnt=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++){
  a[i]=lower_bound(b+1,b+1+cnt,a[i])-b;
}

解释:

  • b数组存放原值,a数组存放离散化后的值
  • 由于 u n i q u e unique unique 函数的特殊性,我们需要先排序
  • 由于 l o w e r b o u n d lower_bound lowerbound 返回的是地址,而我们需要的是它在数组里的排位,于是需要 “-b”

可持久化权值线段树的实现

如上所述,它可以解决求取区间第K小的问题
如题:可持久化线段树2

思路

我们知道,之所以权值线段树不可以实现求解区间第K小的问题是因为他无法保存原数组各个数的排位,而我们是否可以利用可持久化解决这个问题呢?这时候我们会发现因为有了可持久化,权值线段树多了一个东西———版本。
也就是说,我们可以用版本来维护原数组的各个数的顺序,于是乎,我们一个接一个的把原数组里的数按照数组中下标排位插入到空树中,形成了一个又一个版本的权值线段树,这样子,我们就可以用版本来维护原数组中各个数的顺序了。
接下来我们需要考虑怎么求解区间K小值,权值线段树可以求解值域范围内所有数的K小值———那么我们可不可以把这个区间变成值域呢?
可以,只要把其他的数都删除了就可以了。
这个想法看似很疯狂且不切实际,可是我们是有版本的,所以说可以实现。假设我们要求解区间[l,r]中的K小值,那么我们就选取插入l-1的版本和插入r的版本,将它们相减(插入r的版本-插入l-1的版本),此时所有在l之前的数对权值线段树造成的影响全部消失了,只剩下了[l,r]的值域,也就是说,这时,我们就把这个权值线段树的值域变成了[l,r]的值域。

代码实现

建树

首先,我们需要建立一个空树(大部分主席树都是这样的)。

int build(int l,int r){
	countf++;
	int num=countf;
	if(l==r){
		return num;
	}
	t[num].l=build(l,mid(l,r));
	t[num].r=build(mid(l,r)+1,r);	
	return num;
}
更新数值

然后再一一插入数值,建立一个又一个版本的权值线段树:

int update(int k,int l,int r,int x){
	int num=++countf;
	t[num]=t[k];
	t[num].v++;
	if(l==r){
		return num;
	}
	if(x<=mid(l,r)) t[num].l=update(t[k].l,l,mid(l,r),x);
	else t[num].r=update(t[k].r,mid(l,r)+1,r,x);
	return num;
}

主函数部分:

for(int i=1;i<=n;i++){
	a[i]=lower_bound(b+1,b+1+cnt,a[i])-b;//离散化
	root[i]=update(root[i-1],1,cnt,a[i]);
}

查询

很显然,我们需要保存两个版本,然后需要将数值相减,就可以得到在某一值域内数值的个数(权值线段树求解)

int query(int v1,int v2,int l,int r,int x){
	if(l==r) return l;
	int sum=t[t[v2].l].v-t[t[v1].l].v;
	if(sum>=x) return query(t[v1].l,t[v2].l,l,mid(l,r),x);
	else return query(t[v1].r,t[v2].r,mid(l,r)+1,r,x-sum);
}

完整code(包含离散化)

#include<stdio.h>
#include<algorithm>
#define N 200005
using namespace std;
struct {
	int l,r,v;
}t[N*20];
int a[N],root[N],b[N];
int countf;
int mid(int l,int r){
	return (l+r)>>1;
}
int build(int l,int r){
	countf++;
	int num=countf;
	if(l==r){
		return num;
	}
	t[num].l=build(l,mid(l,r));
	t[num].r=build(mid(l,r)+1,r);	
	return num;
}
int update(int k,int l,int r,int x){
	int num=++countf;
	t[num]=t[k];
	t[num].v++;
	if(l==r){
		return num;
	}
	if(x<=mid(l,r)) t[num].l=update(t[k].l,l,mid(l,r),x);
	else t[num].r=update(t[k].r,mid(l,r)+1,r,x);
	return num;
}
int query(int v1,int v2,int l,int r,int x){
	if(l==r) return l;
	int sum=t[t[v2].l].v-t[t[v1].l].v;
	if(sum>=x) return query(t[v1].l,t[v2].l,l,mid(l,r),x);
	else return query(t[v1].r,t[v2].r,mid(l,r)+1,r,x-sum);
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		b[i]=a[i];
	}
	sort(b+1,b+n+1);
	int cnt=unique(b+1,b+1+n)-b-1;
	root[0]=build(1,cnt);
	for(int i=1;i<=n;i++){
		a[i]=lower_bound(b+1,b+1+cnt,a[i])-b;
		root[i]=update(root[i-1],1,cnt,a[i]);
	}
	for(int i=1;i<=m;i++){
		int l,r,x;
		scanf("%d%d%d",&l,&r,&x);
		printf("%d\n",b[query(root[l-1],root[r],1,cnt,x)]);
	}
	return 0;
}

这里就是基础的主席树介绍的结尾,谢谢大家的阅读,请在评论区批评指正。

update:

2024/6/16 感谢评论区的留言,代码问题已经修正
在MATLAB中,可以使用fitctree函数生成决策树模型。下面是一个详细的步骤说明: 1. 准备数据:将训练数据保存在一个m×n的矩阵中,其中m是样本数量,n是特征数量。将每个样本的标签保存在一个m维的向量中,表示每个样本的类别。 2. 构建决策树模型:使用fitctree函数来构建决策树模型。该函数的基本语法如下: ```matlab tree = fitctree(data, labels); ``` 这里,data是训练数据矩阵,labels是对应的标签向量。fitctree函数会自动根据训练数据和标签构建出一棵决策树,并返回一个分类器对象tree。 3. 可选:设置决策树模型的参数:fitctree函数支持一些可选参数,可以根据需要进行设置。例如,可以设置最大深度、最小叶子数、分裂准则等。例如: ```matlab tree = fitctree(data, labels, 'MaxDepth', 5, 'MinLeafSize', 10); ``` 4. 可选:使用交叉验证选择最优参数:如果需要选择最优的参数设置,可以使用交叉验证。可以通过创建一个模板,然后使用fitcecoc函数进行交叉验证选择最佳参数。例如: ```matlab t = templateTree('MaxNumSplits', 'all'); tree = fitcecoc(data, labels, 'Learners', t, 'CrossVal', 'on'); ``` 5. 可选:绘制决策树图形:可以使用view函数来可视化生成的决策树模型。例如: ```matlab view(tree, 'Mode', 'graph'); ``` 6. 使用训练好的决策树模型进行预测:将测试数据保存在一个p×n的矩阵中,其中p是测试样本数量,n是特征数量。使用predict函数对测试数据进行分类预测,得到预测结果。例如: ```matlab predictions = predict(tree, testData); ``` 这里,tree是训练好的决策树模型,testData是测试数据矩阵。 通过以上步骤,你可以在MATLAB中生成决策树模型,并用于分类任务。根据具体需求,可以调整参数和参数设置来优化决策树模型的性能。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值