【数据结构】浅谈主席树

前置知识

①线段树

②权值线段树

③桶的思想

④前缀和思想

(以上几个前置知识我也希望我能有时间写写自己的博客讲解一下【如果有时间的话呜噫呜噫~)

模板题

先上几道模板题压压惊,有从别的博主那里piao来的,也有自己做到的~

因为深刻感受到了,要学习一个东西,最好还是先看看博客,看看思想,看看代码实现

然后!拿着你热乎的手敲模板去A它个几道模板题考验一下你的板子,再继续深刻理解一下这个算法的精髓,哦~完美!

P3919 【模板】可持久化线段树 1(可持久化数组)

P3834 【模板】可持久化线段树 2(主席树)

P1801 黑匣子

P5838 [USACO19DEC]Milk Visits G(这道题不算模板,但是还蛮有意思,maybe你模板题都写过之后可以思考一下这道题,然后敲一敲检验一下自己是不是真的会了,这道题是dfs+lca+主席树的,但是解法不仅限于这一种哦【队里大佬就有种超级巧妙超级厉害的解法dfs+lca就可以啦,下次有空也打算把题解写写博客】)

主席树的含义

主席树就是可持久化线段树,它是一种可持久化的数据结构。

那什么叫做可持久化的数据结构呢?

可持久化数据结构就是支持历史询问的数据结构。

比如一共有54115411次操作,我问你第251251次操作之后这个数据结构长啥样,你能在约束的时间空间内回答出来就算支持了可持久化,否则就不算。

一种很××的做法就是每次更改构之后我都把它保存下来,然后你问哪次我就去哪次里面找就是了。但是这显然在空间上非常不优秀。

然后前辈们发现,每次修改只会让该数据结构某部分与之前不同,那就只需要记录这不同的部分就行了。

——引用自浅谈主席树

能解决哪些问题

本质上是为了做 给你一个序列,每次修改后算一个新的版本,询问某个版本中某个值 这个问题的,但是这个问题衍生开来可以演变出很多问题,比如很经典的主席树模板题区间第k大问题

主席树的原理

普通的线段树能够维护当前状态,而主席树能够维护 当前状态+历史状态

我根据身边老板的反应以及自己第一次接触线段树的感受,猜测应该很多人第一次听到历史状态这个词都会特别懵逼,那就举个例子来具体说明吧~

栗子

比如有一个数组,有n个数,分别是a1,a2,...,an,有以下几种操作:

只要修改一次数组的值就会变成一个新的状态(第一次更新后为第一个状态,第二次更新后为第二个状态,以此类推

①对第 i 个状态进行单点修改,把第 i 个状态时,第pos个位置变为k。

②查询第 i 个状态时,第pos个位置上的值。

像我们平时用线段树做的题目,那都是在当前基础上进行修改,这个基础就是前面进行过的所有操作综合的结果,要问你第某某次修改之后,第某某个结点的值,我想你必然是答不出来了。

暴力

现在我们先来想一个非常暴力的方法来解决这个想要在历史版本上进行修改/查询操作的问题吧~

既然我们可以做到在当前状态下修改啊、查询啊,说明对于修改查询其实不是问题,我们对于当前状态进行修改查询需要一颗线段树来维护。那对于上面提出的历史版本的问题,我们就用好多好多颗线段树就可以解决啦。

想象一下,你每修改一次(数组发生改变)后,你就用一颗新的线段树来保存修改后这个数组的状态。一棵线段树对应一个历史版本,那你需要在某个历史版本上进行修改或者查询操作的时候是不是只需要找到这个历史版本对应的这棵线段树,然后在这棵线段树上操作就完事了。

当然,这样问题倒是解决了,空间也是爆炸了,还是炸的稀碎的那种hhhhhhhh

那怎么优化呢?

空间优化(核心思想一)

那就要用到主席树的第一个核心思想——空间优化

因为我们知道线段树是一个二叉树维护状态,你每一次修改最多会修改掉logN个结点(N是整棵线段树的节点总数),也就是修改掉从你修改的这个叶子结点一路往上走,走到根结点的这条链会发生变化,其他结点都没有发生变化。因此每一次修改就只需要新建logN个结点供新的这棵线段树使用,其他的结点跟之前的线段树共用就可以啦,这样是不是一下子就省了好多好多空间!

这样,如果有m次修改,那 空间复杂度就是N+mlogN 的,是不是非常理想,非常诱人的一个空间复杂度!

(菜鸡第一次自己正儿八经算空间复杂度,如有不对之处,还请各位大佬不吝赐教~)

图解主席树

举个栗子说明一下刚刚说的空间优化的过程哈

序列 4 3 2 3 6 1

根据权值线段树的思想,以值域作为线段树的根结点的区间

建一棵如下图的权值线段树

在这里插入图片描述

一开始build完一棵初始的树,都是空的,里面啥子都没得。

然后开始把我们序列里的点一个一个插入进去,先插入第一个数4

首先,先新建一个点作为根节点,因为不管修改哪一个点,根节点一定会被修改掉,因为根节点是掌管整个值域的。

然后看看4是属于原先那棵树的哪个儿子呀——右儿子,所以我们要新建一个结点作为新的根节点的右儿子,左儿子没有被修改,所以新的根结点跟之前版本的根结点共用一下就可以啦

递归到下面也是同理哦,被修改了的话就新建一个,没有的话就共用,nice!

img

插入第2个数 3 的时候是在已经插入了4这个数的基础上修改,也就是在蓝色点的基础上修改,原理同上

在这里插入图片描述

同上

在这里插入图片描述

图解大概就是这样哦

甚至可以结合代码来康康!可能会理解得更快一点我猜

图源【学习笔记】主席树

代码实现

讲完了主席树的核心思想,就得讲讲代码实现了。

个人学习主席树最痛苦的经历就是看不懂博主们的模板~呜噫呜噫

所以我觉得学会思想是一回事,把思想跟代码结合起来理解就是另外一回事了

所以讲代码也是很重要的!

所以,我掏出了我四十米的大刀(啊呸,长长的主席树板子

往上面加上了罗里吧嗦的注释

希望能够帮助各位理解吧

不过感觉这样比较适合初学者理解代码

用的时候这么多注释好不优雅哦hhhhhhhhh

【忘记说了】这个板子是直接拉了一个模板题的代码,大家可以根据这道题目理解康康

传送门

#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<iostream>
#include<map>
#include<queue>
#include<string>
using namespace std;
typedef long long ll;
const ll maxn=1e6+50;

struct node
{
	ll l,r,v;
}tree[maxn<<5];
//node是树上的结点,l代表其掌管的区间的左边界,r代表其掌管的区间的右边界 

ll rt[maxn],sz=0;
//rt是记录每个版本的那棵线段树的根节点的数组,rt[0]代表第0个状态的线段树的根节点的节点编号为rt[0]
//sz是记录当前用了多少个结点的,也是每次新增结点时使用的变量~ 
ll a[maxn];//记录初始数组,初始建树的时候用 
void build(ll &rt,ll l,ll r)//建树 
{
	rt=++sz;//新建一个结点 
	if(l==r)
	{
		tree[rt].v=a[l];
		//碰到一个结点只掌管一个值的时候,
		//把初始值更新上去(讲不太灵清,希望学过线段树的大家都懂 
		//build函数跟普通线段树没什么区别 
		return;
	}
	ll mid=(l+r)>>1;
	build(tree[rt].l,l,mid);//建左子树 
	build(tree[rt].r,mid+1,r);//建右子树 
}

ll update(ll o,ll l,ll r,ll pos,ll k)//更新|在版本o上把pos这个位置的值更新为k
{
	ll oo=++sz;//新建一个节点作为这个新的状态的根节点 
	tree[oo]=tree[o];//首先把原先的根结点整个赋给新的根节点 
	if(l==r)
	{
		tree[oo].v=k;//如果找到更新点则更新这个点的值 
		return oo;
	}
	ll mid=(l+r)>>1;
	if(mid>=pos)tree[oo].l=update(tree[oo].l,l,mid,pos,k);
	//判断pos是否大于mid,若是则说明应该向这个结点的左子树去更新 
	//同时也说明,会被改变的那条链是在左子树上,因此是tree[oo].l=update(....)
	//根据update函数我们知道,该函数会返回一个结点编号作为新建的结点给tree[oo].l 
	//tree[oo].r一开始是从原先的根结点接过来的,在这种情况下,右子树不会发生改变,所以不需要更新,与原来的树公用即可 
	else tree[oo].r=update(tree[oo].r,mid+1,r,pos,k);//同理 
	return oo;//把根结点的编号返回
}

ll query(ll o,ll l,ll r,ll pos)//询问|在版本o上查询pos这个位置的值  
{
	if(l==r)return tree[o].v;//如果找到这个结点则返回这个结点的值 
	ll mid=(l+r)>>1;
	if(mid>=pos)return query(tree[o].l,l,mid,pos);//如果pos这个位置>=mid则往左子树去找 
	else return query(tree[o].r,mid+1,r,pos);//否则往右子树去找
	//(这部分类似普通线段树) 
}

int main()
{
	ll n,m;
	scanf("%lld %lld",&n,&m);
	for(ll i=1;i<=n;i++)
	{
		scanf("%lld",&a[i]);//初始序列放在A数组里 
	}
	build(rt[0],1,n);//建树 
	
	//这道题的历史版本的解释跟博客里说的不太一样 
	//这道题目里读入一个操作,无论是查询还是更新, 
	//执行完这个操作之后就是一个新的状态了 
	for(ll i=1;i<=m;i++)
	{
		ll v,op,pos,k;
		scanf("%lld %lld %lld",&v,&op,&pos);
		if(op==1)//更新操作 
		{
			scanf("%lld",&k);
			rt[i]=update(rt[v],1,n,pos,k);//在版本v上把pos这个位置的值更新为k 
		}
		else//查询操作 
		{
			rt[i]=rt[v];
			printf("%lld\n",query(rt[v],1,n,pos));//在版本v上查询pos这个位置的值 
		}
	}
}

闲话家常

从某个博主那里看到,据说主席树叫做主席树的原因是发明它的人叫做黄嘉泰,缩写HJT,因此得名主席树~好有意思嘻嘻嘻嘻

听说主席树是有什么静态啊,动态啊的,就暂时没往后学了,先学到这里啦~

以后有空再补充哦~

听说主席树也是可以区间修改什么的,但是可能会比较复杂,复杂度什么也会比较高,不太经常用到,所以暂时也没看先。

关于主席树如何解决区间第k大问题,我想再写一篇博客来讲,所以静待吧~

写完会把链接放上来的~

参考博客

这些是我在学习主席树的时候看的一些博客,可能会对你萌有用,就链在这里啦

其实也是为了自己以后还能找到它们,毕竟里面还有一些没A过的模板题呢~

然后因为我个人是特别懒的,所以直接抓了博主们的图来用,特此声明~

【学习笔记】主席树

主席树入门详解+题目推荐

浅谈主席树

主席树详解

主席树 (动态)图文讲解让你一次就懂 zoj2112为例

posted @ 2020-07-14 19:05  AnranWu  阅读(374)  评论(0编辑  收藏  举报