左偏树初探 By cellur925

$Intro$

假如有两个堆,我们想把它俩合并,怎么搞?

其实不用把两个堆中的元素一个一个抠出来啦x,我们有更厉害的数据结构--可并堆!

今天我们就来看看可并堆中最亲民的一种--左偏树!


$Properties$

  • 堆的性质
    •   既然他是一个(可并)堆,那它一定有堆的性质啦qwq。以小根堆为例,节点的权值小于等于它左右儿子的权值。
  • 树的性质
    •   准确地说它是一个二叉树qwq。又因为它的名字:左偏,所以还有一些特殊性质。
    •        我们先来定义一个量:距离。这里的距离表示此节点到它子树中最近叶子节点的距离。叶子节点的距离为0。
    •        而左偏树满足这样的性质:节点的左儿子的距离大于等于右儿子的距离。


比如这张图图自@CrazyAC,侵删

可以把它感性理解为一个半身不遂向左偏的病人(逃)


$Operation$

在介绍具体操作之前,我们先来看看为了维护上述性质我们需要保存记录一些什么量。

左右儿子的编号($lch$,$rch$)。节点的权值($val$)。节点的距离(意义见上)($dis$)。

另外还需要$fa$来存储父节点编号(直接父节点,注意与并查集区分)(至于为什么需要它,我们接下来再说)

下面我们来seesee几个基本操作:合并、插入、删除堆顶。

  • 合并
    •   由于我们在合并的操作给定的输入大多都是“合并$x$,$y$点所在的堆”,那么我们就需要$fa$来记录$x$,$y$的位置关系,但是这里注意,我认为它并不是并查集。因为并查集通常都有路径压缩,在压完(随时更新)后还要把$fa$值更新为$getf(x)$。
int getf(int x)//并查集
{
    if(x==fa[x]) return x;
    else return fa[x]=getf(fa[x]);//注意这里!
}

也就是说,并查集中的$fa$是随时需要更新成总父亲的。而我们左偏树中的$fa$永远是直接(临时)父亲,不会改变,$getf$操作,只是用$x$自己去找到自己的总父亲(暴力向上跳)

左偏树的$getf$操作:

int getf(int x)
{
    while(fa[x]) x=fa[x];
    return x;
}
    •   所以说,我们合并的,其实是两个待查询节点的总父亲代表的两个待并堆。

主程序中的合并

scanf("%d%d",&x,&y);
if(val[x]==-1||val[y]==-1) continue;//如果已经删除过,不予操作
int xx=getf(x);
int yy=getf(y);
if(xx==yy) continue;
merge(xx,yy);
            
    •        有了上述思想,剩下的我们就只是维护我们之前所说的性质了。

合并函数

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);//维护小根堆性质
    //这里想让x当堆顶 (根) 
    rch[x]=merge(rch[x],y);//将y所在堆与x的右儿子部分的堆合并 
    fa[rch[x]]=x;//记录右儿子的父亲 即x 
    if(dis[lch[x]]<dis[rch[x]])
        swap(lch[x],rch[x]);//维护左偏性质 
    dis[x]=dis[rch[x]]+1;//更新距离性质 
    return x;
}
  • 插入
    •   插入一个点,就是把一个点和一个树合并起来。
  • 删除一个数
    •   合并它的左右儿子
void pop(int x)
{
    val[x]=-1;
    fa[lch[x]]=fa[rch[x]]=0;
    merge(lch[x],rch[x]);
}

 

待填坑

posted @ 2018-10-14 08:40  cellur925&Chemist  阅读(143)  评论(0)    收藏  举报