[2018.12.6]左偏树

其实NOIp之前就学会了...结果咕到了现在...

我们都知道堆。但是很少有人会手写堆。因为我们有STL,而且手写堆码量不小(据说是吧?没写过)。

而且堆的(Merge)操作又慢又麻烦。

于是就有了可并堆。

即使c++也有自带的可并堆

左偏树就是其中之一。

Luogu模板题链接

什么是左偏树

就是一颗向左偏的树。(逃

看一个例子:

图炸了QWQ

(来源:百度图片)

好像确实左偏...

左偏树中,定义一个外节点为:左孩子或者右孩子为空的节点。

而一个节点的距离为它的子树中与它最近的外节点的距离。

外节点的距离为0,空节点的距离为-1(方便计算)。

距离在图中用蓝色字体标出。

接下来的内容中,我们维护大根堆(小根堆类似),对于左偏树的一个节点,其定义如下:

struct Ltree{//丑陋的结构体名
	int v,d,f,c[2];
    //v:节点的值 d:距离 f:父亲 c[0]:左孩子 c[1]:右孩子
}t[Size];//丑陋的变量名

左偏树的性质

左偏性质(不然干嘛叫左偏树):

对于任意节点(x),有(t_{t_x.c_0}.dge t_{t_x.c_1}.d),说人话就是一个节点的左孩子的距离大于它右孩子的距离。

这个性质保证左偏树的时间复杂度(介绍合并操作的时候会让大家感性理解这一点)。

堆性质(不然干嘛叫可并堆):

对于任意节点(x),有(t_x.vge t_{t_x.c_0}.v),(t_x.vge t_{t_x.c_1}.v),说人话就是任何一个节点的值大于等于它所有孩子的值。

这个性质保证左偏树的正确性。

左偏树的操作

注意:如果像模板题中一样需要判断是否被删除,需要另行记录。

找根操作((Find(x)))

找到(x)节点的根。

不停跳(t_x.f)即可。

code:

int Find(int x){
    while(t[x].f)x=t[x].f;
    return x;
}

合并操作((Merge(x,y)))

将以(x,y)为根的堆并在一起。

由于要维护堆性质,合并它们时,要让值大的在上面。

我们假设(x)为根,那么如果(t_x.v<t_y.v),就交换(x)(y)

然后呢?不用管然后了,直接递归到(Merge(t_x.c_1,y))即可。

注意这里我们把(y)(t_x.c_1)合并,而不是(t_x.c_0),就是因为左偏性质,右边的距离更小,在右边进行合并可以维持它的平衡,确保复杂度。(你要我严谨证明我也不会啊...)

然后此时(t_x.c_0)的距离有可能小于(t_x.c_1),如果出现这种情况,交换(x)的左右儿子。

然后需要更新(x)的距离。

显然(t_x.d=t_{t_x.c_1}.d+1)。应该很容易理解。

code:

void Merge(int &x,int &y){
	if(!y)return;//结束
	if(!x){//结束
		swap(x,y);
		return;
	}
	if(t[x].v<t[y].v)swap(x,y);//维护堆性质,交换x,y
	Merge(t[x].c[1],y);
	t[t[x].c[1]].f=x;//更新右孩子的父亲
	if(t[t[x].c[0]].d<t[t[x].c[1]].d)swap(t[x].c[0],t[x].c[1]);//维护左偏性质,交换x的左右孩子
	t[x].d=t[t[x].c[1]].d+1;//更新距离
}

删除操作((Delete(x)))

删除节点(x)所在堆的最大值。

找到(x)所在左偏树的根,将根的左右孩子的父亲设为空,合并它们即可。

code:

void Delete(int x){
	t[t[x].c[0]].f=t[t[x].c[1]].f=0;
	Merge(t[x].c[0],t[x].c[1]);
}
Upd(2018.12.12)

才知道删除还可以删除任意已知节点。。。

这里的已知指我们可以直接访问它的位置,而不是知道它的值。比如左偏树不能完成例如删除树中值为233的节点,但是可以进行例如删除编号为233的节点(我们可以直接访问233号节点)。

类似删除根的方法,我们先合并它的左右子树,然后一直跳它的父亲:

当他父亲的距离等于新合并的子树的距离+1,就可以停止了;

当它的父亲的距离大于新合并的子树的距离+1,更新父亲的距离,如果这棵子树是父亲的左子树的话还需要交换父亲的子树;

当它的父亲的距离小于新合并的子树的距离+1,如果这棵子树是父亲的左子树就停止,否则更新父亲的距离为两棵子树距离的较小值+1,如果左子树的距离更小就交换左子树。

因为作者很懒所以代码不给出。

最值操作((Max(x)))

返回(x)所在左偏树的最大值。

也就是返回根的值啦!

code:

int Max(int x){
	return t[Find(x)].v;
}

没了。

这个数据结构是如此的简单。

还有一点要说的:关于给一个序列建左偏树。

给每个元素建一棵一个节点的左偏树,顺序合并即可。时间复杂度(O(nlogn))

讲完了。

最后附上模板题代码:

#include<bits/stdc++.h>
using namespace std;
struct Pair{
    int v,id;
    bool operator >(Pair y){
        if(v!=y.v)return v>y.v;
        return id>y.id;
    }
    bool operator <(Pair y){
        if(v!=y.v)return v<y.v;
        return id<y.id;
    }
};
struct node{
    int f,d,c[2];
    Pair v;
}t[100010];
int n,m,op,u,v,fu,fv,del[100010];
int getf(int x){
    while(t[x].f)x=t[x].f;
    return x;
}
void Merge(int &x,int &y){
    if(!y)return;
    if(!x){
        swap(x,y);
        return;
    }
    if(t[x].v>t[y].v){
        swap(x,y);
    }
    Merge(t[x].c[1],y);
    t[t[x].c[1]].f=x;
    if(t[t[x].c[1]].d>t[t[x].c[0]].d){
        swap(t[x].c[1],t[x].c[0]);
    }
    t[x].d=t[t[x].c[1]].d+1;
}
int Delete(int x){
    t[t[x].c[0]].f=t[t[x].c[1]].f=0;
    Merge(t[x].c[0],t[x].c[1]);
    del[x]=1;
    return t[x].v.v;
}
int main(){
    scanf("%d%d",&n,&m);
    t[0].d=-1;
    for(int i=1;i<=n;i++){
        scanf("%d",&t[i].v.v);
        t[i].v.id=i;
        t[i].d=0;
    }
    for(int i=1;i<=m;i++){
        scanf("%d",&op);
        if(op==1){
            scanf("%d%d",&u,&v);
            fu=getf(u);
            fv=getf(v);
            if(del[u]||del[v]||fu==fv)continue;
            Merge(fu,fv);
        }else{
            scanf("%d",&u);
            if(del[u]){
                puts("-1");
            }else{
                fu=getf(u);
                printf("%d
",Delete(fu));
            }
        }
    }
    return 0;
}

练习题:

[APIO2012]派遣

->Luogu

->BZOJ

->题解

原文地址:https://www.cnblogs.com/xryjr233/p/10536872.html