Live2D

浅谈主席树(可持久化线段树)

主席树(可持久化线段树)

前言:

建议先掌握线段树再来学习此知识点
这几天在中山纪中训练时有做到可持久化字典树的题,听别人说可持久化数据结构的思路都是差不多的,于是我便先上网学了学我看起来认为最简单的可持久化线段树(主席树),学了两个晚上终于学会了一点皮毛 (~ ̄▽ ̄)~,于是我便来总结一下理理思路
博客可能写的一般大佬勿喷……

名字分析:

顾名思义,可持久化的就是可以记录历史版本的意思,通过主席树,我们可以查询到单个节点历史的信息(或者也可以做到静态区间查询),或者对于某个历史版本进行单点修改产生新的版本。(如果想进行其它更难的操作可以使用树套树,可是我太弱了暂时还不会)
至于主席树这个名字嘛,据说时发明这个东西的人的姓名缩写是HJT,跟当时国家主席的姓名缩写一样,于是便有了这个说法……

引导:

从这个名字看来,知道主席树跟线段树肯定是有联系的,那么我们来思考一下这个问题:如果只用普通的线段树怎么记录历史版本?
最直截了当的方法就是每次操作之后再重新建一棵线段树,数据小,题目空间限制大还可以做;如果数据和空间稍微恶心那么一点点,那么恭喜你,你将同时享受到MLE(空间超限)与TLE(时间超限)。
这里我画个图来示意一下线段树单点修改(查询)的过程(区间同理)
假设有一个数列长度为5,当前要修改(查询)第2个数,如下图,红色线代表到的区间:
在这里插入图片描述
我们可以发现,其实每次线段树改变的就只有一条链,这很重要,主席树的思想就差不多出来了
对于一次新的修改,我们只需要再次构造一条新的表示这一段区间的链,之后另外节点与前面的共用就行了,我再画一个图示意一下主席树的思想(还是以上面的为例,如下图:)
在这里插入图片描述
为了好看一点我把一些新节点的左右儿子反过来画了(不过还是好丑),大家凑合着看一看吧
所以主席树每一次进行操作,时间和增加的空间都是\(log\)的(树套树的话好像是2个\(log\)的,我也不太清楚,毕竟我还没开始学)

例题:

主席树模板题(因为当前我在gmoj找不到主席树的模板题,于是就用了luogu的)
题意大概是讲一开始给你一个长度为n初始数列,为版本0,之后有m个操作,对于每个操作有两种情况:
1. 改变某一个版本的某一个数,形成一个新的版本
2. 对某一个版本进行单点查询并输出,再生成一个完全一样的版本
(也就是说每次的版本号就是操作的号数)

讲解&代码

既然是模板题,我们就直接上代码和讲解吧

首先明白一个事实,因为主席树是要一些节点共用儿子节点的(也就是说一个节点可能有很多父亲),所以很显然是不能用\(now<<1\)\(now<<1|1\)(也就是堆式结构)来表示节点now的儿子的,而是应该使用动态开点

变量:

struct arr
{
	int lson;//左儿子
	int rson;//右儿子
	int num;//节点值
}tree[40000005];//线段树节点
int n,m,tot,a[1000005]//原始数组;
int root[1000005];//每个版本的线段树根的编号

建树:

对于建树操作,跟线段树很类似,只不过是动态开点

void build(int &now,int l,int r)//这里的now是加了取址符的,所以这个传进去的变量也会随之改变
{
	now=++tot;//动态开点
	if (l==r) tree[now].num=a[l];//叶子节点赋值
	else
	{
		int mid=l+r>>1;
		build(tree[now].lson,l,mid);
		build(tree[now].rson,mid+1,r);//注意不能用now<<1和now<<1|1(也就是堆式结构)
	}
}

修改

对于修改操作,我们只是修改一条链,所以只用查找要修改的点进行新建节点,每次递归下来一个区间,就复制一份原线段树中当前节点的值,其它操作就跟线段树差不多一样了

void change(int last,int &now,int l,int r,int k,int x)//last表示上一个版本的线段树
{
	now=++tot;
	tree[now]=tree[last];//复制一份上一个版本的对应节点
	if (l==r) tree[now].num=x;
	else
	{
		int mid=l+r>>1;
		if (k<=mid) change(tree[last].lson,tree[now].lson,l,mid,k,x);
		else change(tree[last].rson,tree[now].rson,mid+1,r,k,x);//判断要修改的点是在左区间还是右区间
	}
}

查询

这基本就跟线段树一模一样了

int find(int now,int l,int r,int k)
{
	if (l==r) return tree[now].num;
	else
	{
		int mid=l+r>>1;
		if (k<=mid) return find(tree[now].lson,l,mid,k);
		else return find(tree[now].rson,mid+1,r,k);
	}
}

总代码

#include<bits/stdc++.h>
using namespace std;
struct arr
{
	int lson;//左儿子
	int rson;//右儿子
	int num;//节点值
}tree[40000005];//线段树节点
int n,m,tot,a[1000005]//原始数组;
int root[1000005];//每个版本的线段树根的编号
void build(int &now,int l,int r)//这里的now是加了取址符的,所以这个传进去的变量也会随之改变
{
	now=++tot;//动态开点
	if (l==r) tree[now].num=a[l];//叶子节点赋值
	else
	{
		int mid=l+r>>1;
		build(tree[now].lson,l,mid);
		build(tree[now].rson,mid+1,r);//注意不能用now<<1和now<<1|1(也就是堆式结构)
	}
}
void change(int last,int &now,int l,int r,int k,int x)//last表示上一个版本的线段树
{
	now=++tot;
	tree[now]=tree[last];//复制一份上一个版本的对应节点
	if (l==r) tree[now].num=x;
	else
	{
		int mid=l+r>>1;
		if (k<=mid) change(tree[last].lson,tree[now].lson,l,mid,k,x);
		else change(tree[last].rson,tree[now].rson,mid+1,r,k,x);//判断要修改的点是在左区间还是右区间
	}
}
int find(int now,int l,int r,int k)
{
	if (l==r) return tree[now].num;
	else
	{
		int mid=l+r>>1;
		if (k<=mid) return find(tree[now].lson,l,mid,k);
		else return find(tree[now].rson,mid+1,r,k);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	int i;
	for (i=1;i<=n;i++)
		scanf("%d",&a[i]);
	build(root[0],1,n);//建树存到版本0中
	for (i=1;i<=m;i++)
	{
		int v,cz,loc;
		scanf("%d%d%d",&v,&cz,&loc);
		if (cz==1)
		{
			int value;
			scanf("%d",&value);
			change(root[v],root[i],1,n,loc,value);//修改操作
		}
		else
		{
			printf("%d\n",find(root[v],1,n,loc));//查询操作
			root[i]=root[v];//生成一个一模一样的新版本(题目要求)
		}
	}
	return 0;
}

结语:

由于本人还没有深入去学主席树(可持久化线段树),所以一些比较难的操作我还不是很会(主要是没有尝试码过) (~ ̄▽ ̄)~,不过我这次写得很用心,如果有问题望各位大佬指出,或者如果大家有问题也可以评论下来,如果是我懂的我会尽力帮助大家解决,谢谢!

posted @ 2020-08-11 07:30  冷笑叹秋萧  阅读(216)  评论(1编辑  收藏  举报