浅谈左偏树入门

前言

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

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

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

引子 左偏树是什么?

左偏树是什么?

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

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

与左偏树相关的一些定义

外节点

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

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

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

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

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

距离

左偏树一个很重要的概念就是节点的距离,我们可以将其记作(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)新的右儿子。

为什么要选右儿子?

因为左偏树的左偏性啊!

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

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

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

代码如下:

inline int Merge(int x,int y)//将根节点为x和y的两棵左偏树合并
{
	if(!x||!y) return x+y;//如果存在空节点,直接退出函数
	if(node[x].Val>node[y].Val) swap(x,y);//比较权值大小,维护堆性质
	if(node[node[x].Son[1]=Merge(node[x].Son[1],y)].Father=x,node[node[x].Son[0]].Dis<node[node[x].Son[1]].Dis) swap(node[x].Son[0],node[x].Son[1]);//将x的右儿子取出与y进行合并,然后比较左右儿子距离,维护左偏性
	return node[x].Dis=node[node[x].Son[1]].Dis+1,x;//更新距离,返回合并后的根节点x
}

关于左偏树的其他操作

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

  • 删除

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

inline void PopTop(int x)//弹出堆顶 
{
    	if(!node[x=TopPos(x)].Exist) return;//如果堆顶不存在,退出函数
  	node[x].Val=node[x].Dis=-1,node[x].Exist=0,node[node[x].Son[0]].Father=node[node[x].Son[1]].Father=0,//清空信息
  	Merge(node[x].Son[0],node[x].Son[1]);//合并两棵子树
}
  • 求堆顶元素值

    我们可以对每一个节点记录它的(Father),然后不断往上跳即可。代码如下:

inline int TopPos(int x) {while(node[x].Father) x=node[x].Father;return x;}//求出堆顶元素的编号
inline int TopVal(int x) {return node[TopPos(x)].Val;}//求出堆顶元素的值 

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

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

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

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

#include<bits/stdc++.h>
#define N 100000
#define swap(x,y) (x^=y^=x^=y)
using namespace std;
int n,a[N+5];
class FIO
{
    private:
        #define Fsize 100000
        #define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,Fsize,stdin),A==B)?EOF:*A++)
        #define pc(ch) (void)(FoutSize<Fsize?Fout[FoutSize++]=ch:(fwrite(Fout,1,Fsize,stdout),Fout[(FoutSize=0)++]=ch))
        int Top,FoutSize;char ch,*A,*B,Fin[Fsize],Fout[Fsize],Stack[Fsize];
    public:
        inline void read(int &x) {x=0;while(!isdigit(ch=tc()));while(x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));}
        inline void write(int x) {if(!x) return pc('0');if(x<0) pc('-'),x=-x;while(x) Stack[++Top]=x%10+48,x/=10;while(Top) pc(Stack[Top--]);}
        inline void write_char(char x) {pc(x);}
        inline void clear() {fwrite(Fout,1,FoutSize,stdout);}
}F;
class Class_LeftistTree
{
    private:
        struct Tree
        {
            int Val,Dis,Father,Exist,Son[2];
            Tree(int x=0):Val(x){Dis=Father=Son[0]=Son[1]=0,Exist=1;}
        }node[N+5];
        inline int Merge(int x,int y)
        {
            if(!x||!y) return x+y;
            if(node[x].Val>node[y].Val||(node[x].Val==node[y].Val&&x>y)) swap(x,y);
            if(node[node[x].Son[1]=Merge(node[x].Son[1],y)].Father=x,node[node[x].Son[0]].Dis<node[node[x].Son[1]].Dis) swap(node[x].Son[0],node[x].Son[1]);
            return node[x].Dis=node[node[x].Son[1]].Dis+1,x;
        }
        inline int TopPos(int x) {while(node[x].Father) x=node[x].Father;return x;}
    public:
        Class_LeftistTree() {node[0].Dis=-1,node[0].Exist=0;}
        inline void Init(int n,int *data) {for(register int i=1;i<=n;++i) node[i]=Tree(data[i]);}
        inline void Union(int x,int y) {if((x=TopPos(x))^(y=TopPos(y))&&node[x].Exist&&node[y].Exist) Merge(x,y);}
        inline void PopTop(int x) {if(node[x=TopPos(x)].Exist) node[x].Val=node[x].Dis=-1,node[x].Exist=0,node[node[x].Son[0]].Father=node[node[x].Son[1]].Father=0,Merge(node[x].Son[0],node[x].Son[1]);}
        inline int TopVal(int x) {return node[TopPos(x)].Val;} 
}LeftistTree;
int main()
{
    register int i,Q,op,x,y;
    for(F.read(n),F.read(Q),i=1;i<=n;++i) F.read(a[i]);
    for(LeftistTree.Init(n,a);Q;--Q)
    {
        if(F.read(op),F.read(x),op^1) F.write(LeftistTree.TopVal(x)),F.write_char('
'),LeftistTree.PopTop(x);
        else F.read(y),LeftistTree.Union(x,y);
    }
    return F.clear(),0;
}

关于例题

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

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

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

原文地址:https://www.cnblogs.com/chenxiaoran666/p/LeftistTree.html