把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

浅谈左偏树入门

前言

要维护一段区间内的最值时,我们可以用平衡树)来操作。

但是,如果要合并两个堆,复杂度就极高了。

所以,我们就要使用左偏树这个神奇的数据结构,来实现堆的合并。

引子 左偏树是什么?

左偏树是什么?

当然是向左偏的树。(废话)

实际上,现在来解释左偏树的概念还是有点难的,所以我们要先来看一些定义。

与左偏树相关的一些定义

外节点

外节点,顾名思义,就是在外部的节点。

它的定义是至少一个子节点是空节点的节点是外节点

注意是至少一个,而不是全部,不然就变成叶节点了。

而记录外节点有什么用呢?

当我们需要插入一个节点时,如果找到了外节点,就可以轻松将其插入到这个外节点的某一空子节点。

距离

左偏树一个很重要的概念就是节点的距离,我们可以将其记作\(Dis(x)\)

比较简单,一个节点的距离指的就是它到离它最近的外节点的距离

对于该节点本身是外节点该节点是空节点两种特殊情况,我们分别规定它们的距离为\(0\)\(-1\)

左偏树的一些性质

首先,我们要知道,左偏树是满足堆性质的(废话,它就是用来实现堆合并的)。

而左偏树另一比较重要的性质是左偏性

让我们回到前面的那个问题,什么是左偏树?

现在我们可以对它进行解释了:对于左偏树中的任意一个节点,我们必须满足\(Dis(LeftSon)\ge Dis(RightSon)\),即左儿子离外节点的距离必须大于等于右儿子离外节点的距离

而这就是左偏树的左偏性了。

左偏树其实还有一个比较重要的性质,这个性质在上传信息的操作中起到了重要的作用:对于左偏树中的任意一个节点,\(Dis(x)=Dis(RightSon)+1\)

证明: 根据左偏性,可以得到\(Dis(RightSon)\le Dis(LeftSon)\),而左偏树中距离的定义是一个节点到离其最近的外节点的距离,故为\(Dis(RightSon)+1\)

关于左偏树的合并操作

左偏树的核心操作就是它的合并(毕竟其他操作堆都能轻松实现)。

假设我们要合并两个根节点分别为\(x\)\(y\)的左偏树。

首先,我们要判断\(x\)\(y\)中是否存在空节点,如果有,可以直接退出函数。

然后,我们要比较权值大小(假设是小根堆),如果\(Val_x>Val_y\),则交换\(x\)\(y\)(维护堆性质)。

接下来,我们要将\(x\)的右儿子取出,与\(y\)进行合并,然后将合并后得到的根作为\(x\)新的右儿子。

为什么要选右儿子?

因为左偏树的左偏性啊!

也就是说,选右儿子,可以更早找到空节点将另一棵左偏树插入。

这就保证了左偏树合并操作的时间复杂度。

要注意的是,在合并后,右儿子的距离可能会小于左儿子,此时就需要交换左右儿子,从而维护左偏性。

代码如下:

I int Merge(RI x,RI y)//将根节点为x和y的两棵左偏树合并
{
	if(x==y||!x||!y) return x|y;(O[x].V>O[y].V||(O[x].V==O[y].V&&x>y))&&(swap(x,y),0),//如果有空或相同直接退出,否则比较权值大小维护堆性质
	O[O[x].S[1]=Merge(O[x].S[1],y)].F=x,O[O[x].S[0]].D<O[O[x].S[1]].D&&(swap(O[x].S[0],O[x].S[1]),0);//将x的右儿子取出与y进行合并,然后比较左右儿子距离,维护左偏性
	return O[x].D=O[O[x].S[1]].D+1,x;//更新距离,返回合并后的根节点x
}

关于左偏树的其他操作

理解了合并,左偏树的其他操作就很简单了。

  • 删除

    删除操作,其实就是合并被删除节点的两个子树。代码如下:

I void Pop(int x)//弹出堆顶 
{
	O[O[x].S[0]].F=O[O[x].S[1]].F=0,Merge(O[x].S[0],O[x].S[1]);//合并两棵子树
}
  • 求堆顶元素

    我们可以对每一个节点记录它的\(Father\)

    以前我一直写的是直接不断往上跳,但实际上左偏的那条链可能很长,复杂度假掉了。

    因此我们需要写成并查集形式:

I int getfa(CI x) {return O[x].F?O[x].F=getfa(O[x].F):x;}//并查集

在删除堆顶的时候,我们把原本堆顶的父节点改成新的堆顶,保证并查集的正确性。

因此修改一下删除函数:

I void Pop(int x)//弹出堆顶 
{
	if(!~O[x].V) return;O[x=getfa(x)].V=-1,//删除
	O[O[x].S[0]].F=O[O[x].S[1]].F=0,O[x].F=Merge(O[x].S[0],O[x].S[1]);//合并两棵子树,更新父节点
}

模版题:【洛谷3377】【模板】左偏树(可并堆)

到这里,其实左偏树讲得也差不多了。

相信大家都发现了,左偏树也没有想象中那么难。

下面以这道模板题为例,贴一份完整代码:

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
using namespace std;
int n;
namespace FastIO
{
	#define FS 100000
	#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
	#define pc(c) (FC==FE&&(clear(),0),*FC++=c)
	int OT;char oc,FI[FS],FO[FS],OS[FS],*FA=FI,*FB=FI,*FC=FO,*FE=FO+FS;
	I void clear() {fwrite(FO,1,FC-FO,stdout),FC=FO;}
	Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
	Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
	Tp I void writeln(Ty x) {x<0&&(pc('-'),x=-x);W(OS[++OT]=x%10+48,x/=10);W(OT) pc(OS[OT--]);pc('\n');}
}using namespace FastIO;
class LeftistTree
{
	private:
		struct node {int V,D,F,S[2];}O[N+5];I int Merge(RI x,RI y)//合并
		{
			if(x==y||!x||!y) return x|y;(O[x].V>O[y].V||(O[x].V==O[y].V&&x>y))&&(swap(x,y),0),
			O[O[x].S[1]=Merge(O[x].S[1],y)].F=x,O[O[x].S[0]].D<O[O[x].S[1]].D&&(swap(O[x].S[0],O[x].S[1]),0);
			return O[x].D=O[O[x].S[1]].D+1,x;
		}
		I int getfa(CI x) {return O[x].F?O[x].F=getfa(O[x].F):x;}//找根节点
	public:
		I LeftistTree() {O[0].D=-1;}I void Init(CI x,CI v) {O[x].V=v;}
		I void Union(CI x,CI y) {~O[x].V&&~O[y].V&&Merge(getfa(x),getfa(y));}//合并
		I int Top(CI x) {return ~O[x].V?O[getfa(x)].V:-1;}//求根节点值
		I void Pop(int x)//弹出根节点
		{
			if(!~O[x].V) return;O[x=getfa(x)].V=-1,
			O[O[x].S[0]].F=O[O[x].S[1]].F=0,O[x].F=Merge(O[x].S[0],O[x].S[1]);
		}
}L;
int main()
{
	RI Qt,i,op,x,y;for(read(n,Qt),i=1;i<=n;++i) read(x),L.Init(i,x);
	W(Qt--) read(op),op==1?(read(x,y),L.Union(x,y)):(read(x),writeln(L.Top(x)),L.Pop(x));
	return clear(),0;
}

关于例题

有两道比较好的左偏树例题:

【BZOJ2809】【APIO2012】dispatching:这应该是比较裸的模板题,适合刚学左偏树的人练手。

【BZOJ4003】【JLOI2015】城池攻占:这道题就有一定的扩展了,需要在左偏树上加上懒惰标记,也是挺有意思的题目。

posted @ 2018-11-01 20:13  TheLostWeak  阅读(763)  评论(0编辑  收藏  举报