左偏树详解

引入

左偏树也叫可并堆。
堆想必大家是很熟悉的了,手写可能没有过,但用绝对用过,priority_queue就是STL中的一个二叉堆。
priority_queue和手写的二叉堆差不多,使用起来很方便,平均的时间复杂度都是在O(logN)。
但是一旦要求合并两个堆,我们的priority_queue直接倒下了,因为是STL里面实现的,我们只会pop,top,push,这几个操作,只能把一个堆push进另一个堆里。
手写堆还有点意思,可以搞一搞,考虑考虑集体合并之类的,降低一点复杂度。
不过再怎么优,也要接近O(N)的时间复杂度。不如直接用现成的算法——左偏树。

简介

顾名思义,这是一棵极度左偏的树,有没有右偏树呢用树的话来说就是左子树特别深。

定义

先定义一个“外节点”:左孩子或右孩子为空的节点。
再定义一个“节点的距离”:它到子树内最近的外节点的距离。

性质

1、堆的性质:一个点的点权大于(小于)子节点的点权。
2、左偏性一个点的左子结点的距离不小于右子节点的距离。

基本操作

在定义了这么一棵个性鲜明的树之后,我们就要来介绍树的操作了。

Ⅰ合并

这个当之无愧是最最重要的操作了,这也是二叉堆所不具有的。
左偏树的合并合并的是右子树那条链。
两个大根堆,比较它们根的权值,使得val[x]>val[y](否则交换),然后让x的右子树xr与y合并。同样比较xr和y的权值,让较小与较大的右子树执行合并操作。如此循环下去,直到较大的右子树为空,操作结束。这是一个大致的递归流程。
具体合并是很巧妙的。我自己先瞎JB先定义一个“左树”为x节点及其左子树,“左树根”即为x,方便后面表达。
用程序的话来讲,每次比较两个左树顶的权值,留下一个较大的左树,让比较小的左树和较大的右子树继续合并。
我是这样理解的,一开始比较后确定谁是x谁是y后,把x节点的左树给取出来,放在一边。接着比较的是xr和y,根据权值把xr或者y的左树给取出来,把取出来的那个左树接在x的右孩子位置。然后递归剩下的。相当于说每条左链都以其所处的左树顶为关键字,有点像归并排序的合并操作一样,用左树根表示这一整棵左树的价值,或者说把一棵左树缩成了一个权值为左树根权值的点,这就把两棵左偏树变成两个序列,然后从两个序列中选取较大的数放入新序列。最终会生成一个序列,其中第一个数为根,其余都是右孩子的关系,再把折叠了的左树展开,就是合并后的左偏树了。

再提几个合并的细节。
可以预见最终的合并一定是以x的右子树为0结束的(即ch[x][1]=0),此时应当把剩余的(一定有剩余)y子树接到ch[x][1]这个位置,然后结束递归。按理来说只会出现x=0而不会出现y=0的情况,可是我们的程序却判断了y=0的情况(请稍稍看看代码)。这里先声明如果只有merge操作,只需判断x=0即可。
还有,以上的操作仅仅是维护了左偏树的第1个性质,读者可以用堆的性质来考虑以上的合并,只要明白堆是什么,相信你可以看明白。

好的,下面讲的是左偏性的维护了。
左偏性即dis[ch[x][0]]>=dis[ch[x][1]],所以只要当dis[ch[x][0]]<dis[ch[x][1]]时交换一下左右孩子就好了(放心,这样做它还是一个堆)。
这就没了,说真的
再来就是求出dis[x]就可以了,根据距离的定义及性质2,所以dis[x]的距离一定是从右孩子转移过来的,所以dis[x]=dis[ch[x][1]]+1。

有人会想了,如果某个节点的右孩子是有的,左孩子是空的,如果这时能把这棵树接到左孩子就好了。但是有这回事吗?根据左偏性,设想如果左孩子为空,那么右孩子一定也为空,所以这种想法是多余的。再来补充一下“外节点”的定义,现在发现那些定义“左孩子或右孩子为空”就是多余的,在左偏树中根本3种情况,只有2种:要么一起空,要么右孩子空。
现在再来看看为什么左偏树效率会高。
再提一个性质:一棵n个节点的左偏树距离最多为\left\lfloor \log(n+1) -1\right\rfloor。也就是说一棵左偏树的右树最大深度为\left\lfloor \log(n+1) -1\right\rfloor
细心的读者会发现,我们合并的时间复杂度与右树大小有关,因为我们是对右树做了一次并排序。所以我们希望右树的大小越小越好,这就导致了整棵树结构偏向左边,左树的节点越多,那么右树的节点就越少。这就是维护一个看似不参与任何运算的dis的原因,也是左偏树这么定义dis的原因。

把以上两个合并+维护合在一起,这个操作就大功告成了。

Ⅱ插入

新建一个新的节点,然后merge一下。

Ⅲ删除堆顶

删除堆顶,那么可以把堆顶的左右子树断开与堆顶的父子关系后,merge一下左右节点,相当于把堆顶给孤立了,这时就完成了删除。
这里解决上面提到的问题,由于x是个一般的节点,所以它的左右子树可能为空。如果这时候进行合并,x=0则取y,y=0则取x,若x=0&y=0则为0(空)。所以那两句判断必须一起写着。

Ⅳ求最值

根据堆的性质,最值为堆顶。

模版

例题:洛谷3377 【模板】左偏树(可并堆),此题为小根堆
代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=100010;

int n,m;

int f[MAXN];
int ch[MAXN][2],val[MAXN],dis[MAXN];

int findfa(int x)
{
    while(f[x]) x=f[x];
    return x;
}
int merge(int x,int y)
{
    if(x==0 || y==0) return x+y;//就这两句话都要 
    if(val[x]>val[y] || val[x]==val[y]&&x>y) swap(x,y);//小根堆 
    ch[x][1]=merge(ch[x][1],y);//较小的右子树与较大的继续合并 
    f[ch[x][1]]=x;
    if(dis[ch[x][0]]<dis[ch[x][1]]) swap(ch[x][0],ch[x][1]);//维护左偏性 
    dis[x]=dis[ch[x][1]]+1;
    return x;
}
void pop(int x)
{
    val[x]=-1;
    f[ch[x][0]]=f[ch[x][1]]=0;
    merge(ch[x][0],ch[x][1]);
}

int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&val[i]);
    for(int i=1;i<=m;i++)
    {
        int opt,x,y;
        scanf("%d",&opt);
        if(opt==1)
        {
            scanf("%d %d",&x,&y);
            if(val[x]==-1 || val[y]==-1) continue;
            int fx=findfa(x),fy=findfa(y);
            if(fx==fy) continue;
            merge(fx,fy);
        }
        else
        {
            scanf("%d",&x);
            if(val[x]==-1){puts("-1");continue;}
            int fx=findfa(x);
            printf("%d\n",val[fx]);
            pop(fx);
        }
    }
    return 0;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值