zoukankan      html  css  js  c++  java
  • 浅谈左偏树入门

    前言

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

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

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

    引子 左偏树是什么?

    左偏树是什么?

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

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

    与左偏树相关的一些定义

    外节点

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

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

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

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

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

    距离

    左偏树一个很重要的概念就是节点的距离,我们可以将其记作(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】城池攻占:这道题就有一定的扩展了,需要在左偏树上加上懒惰标记,也是挺有意思的题目。

  • 相关阅读:
    SVN trunk(主线) branch(分支) tag(标记) 用法详解和详细操作步骤
    SVN版本控制—branches、trunk、tag篇
    SVN将项目代码加入svn版本控制
    交换机详细解读
    ESXI
    vSphere
    VM虚拟机?
    Linux 01 LiunxvI命令大全
    “高可用性”(High Availability)??
    说一说,正常上线的流程
  • 原文地址:https://www.cnblogs.com/chenxiaoran666/p/LeftistTree.html
Copyright © 2011-2022 走看看