[StudyNotes] 左偏树

什么是左偏树?

左偏树( Leftisttree )是可并堆( Mergeableheap )的一种

相比于 priorityqueue ,它还支持 Merge (合并两个堆) 操作

左偏树上的每个节点不仅保存了 val ,还存储了 lc (左儿子), rc (右儿子)和 dis

这里的 dis 是什么呢?

首先定义一个“外节点”的概念

如果一个节点 x ,它的左子树或者右子树为空,即还能在 x 这个节点合并上另一个堆,就称 x 为外节点

对于一个节点 x,那么它的 dis 就是这个点到外节点经过的最少的边数

定义外节点的 dis=0

  • 举个栗子:

图中的数字代表每个节点的 dis

pwp

左偏树的性质

这里我用 struct 来存一棵左偏树

struct Leftist_Tree {
    int val,lc,rc,dis;
    bool operator < (const Leftist_Tree &q)const {
        return val<q.val;
    }
};

Leftist_Tree t[MAXN];

这里以小根堆为例

性质 1

fa[x] 表示 x 节点的父亲,那么显然有 t[fa[x]].val<t[x].val(堆的性质)

性质 2

节点的左儿子的距离不小于右儿子的距离

t[lc].dist[rc].dis ,这个性质被称作左偏性质

左偏树,字面上的意思就是向左偏的树,也就是说这棵树左边的节点数一定比较多

那这样的话,每次从右子树找外节点一定比从左子树找外节点快

根据这个还可以得出一个推论:

一个节点的左子树和右子树都是左偏树

性质 3

对于一个有右儿子的节点 x ,有t[x].dis=t[t[x].rc].dis+1

也就是说, 一个节点的 dis 等于它右儿子的 dis+1

为了让这个性质对没有右儿子的节点也满足,我们定义空节点的 dis=1

这样性质3就可以表示为

t[0].dis=1xN+,t[x].dis=t[t[x].rc].dis+1

实在不懂的话,看上面的图的 dis 值就好了

左偏树的一些骚操作

Merge

Merge 可以说是左偏树的核心操作了,其余的操作都是基于 Merge 完成的

上图(图中的数字表示每个节点的 val

qwq

我们要现在合并这两个左偏树,第一步就是找到一个外节点,然后把它并上去

在 性质2 当中我们提到

每次从右子树找外节点一定比从左子树找外节点快

所以我们每次都贪心的找右子树

从根节点开始, 7 是第一个从右子树找到的外节点,而且7<8,这也符合小根堆的性质,好,我们把 8 并到 7 的右儿子上

qwq

这时候重复上面的过程,第一个从右子树找到的外节点是 8 ,而且也符合堆的性质,把 11 并到 8 的右子树上

qwq

这个时候好像是合并完成了,但是其实没有,因为这个时候你会发现这不是左偏树了

我们合并出了一个假的左偏树,GG

我们来想办法让这棵树重新左偏,我们先从 11 回溯到 8 ,发现 8 这个节点并不左偏,因为 t[lc].dis=1,t[rc].dis=0 lc,rc 表示 8 的左右儿子,因为左儿子是空节点,所以t[lc].dis=1),这是不满足性质2的

这时我们为了让树左偏,直接 swap(lc,rc) 就可以了。

这是很显然也是很简单的方法。

swap 之后树变成了这样

qwq

再回溯到 7 ,我们开心地发现 7 也不满足性质2,没事,再 swap

qwq

最后回溯到 5 ,依旧不满足性质2,不怕,swap

qwq

到此为止, Merge 结束了

我们可以发现这其实是一个一直递归回溯判断的过程

然后判断是不是左偏的话,我们利用了性质2

所以 Merge 的代码就可以很自然地写出来了

#define Lc t[x].lc
#define Rc t[x].rc

int fa[MAXN]; 

int Merge(int x,int y) {
    if(!x||!y) return x+y;
    //如果有一个节点是空节点,那么merge之后的根就是x+y
    if(t[y]<t[x]) swap(x,y);
    if(t[x].val==t[y].val&&x>y) swap(x,y);
    //我们在上面的图中,并没有体现出上面的swap
    //因为是小根堆,所以肯定是大的并到小的上面去,所以有if(t[y]<t[x]) swap(x,y);
    //可能写成t[x]>t[y] 好理解一点,但是我只重载了 < ,所以就这么写了
    //第二个if就是让编号大的并到编号小的上去
    Rc=Merge(Rc,y);
    //y一直和x的右子树合并
    fa[Rc]=x;
    //合并了,也不能忘了自己的爸爸是谁
    if(t[Lc].dis<t[Rc].dis) //利用性质2判断是否左偏
        swap(Lc,Rc);
    t[x].dis=t[Rc].dis+1;//利用性质3来更新dis
    return x;//返回合并后的根,方便回溯来维护左偏
}

左偏树经常和并查集结合到一起,因为你要判断合并的点是不是在一棵左偏树里,但是这个并查集不要带路径压缩

也就是说 Find 要这么写

int Find(int x) {
    for(;fa[x];x=fa[x]);
    return x;
}

到这里, Merge 讲完了

pop

pop 操作异常的简单,直接 Merge(Lc,Rc) 就可以了

比较好理解,没什么好说的,注意打一个不在树中的标记和把他清零就好了

贴一下代码

bool not_intree[MAXN];

void pop(int x) {
    not_intree[x]=true;
    fa[Lc]=fa[Rc]=0;
    Merge(Lc,Rc);
    return;
}
insert

insert 相当于把一个只有一个节点的左偏树和整棵左偏树 Merge 就可以了

可以用左偏树来做的题目

简单题

直接贴代码了

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;

template<typename T>
void input(T &x) {
    x=0; T a=1;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar())
        if(c=='-') a=-1;
    for(;c>='0'&&c<='9';c=getchar())
        x=x*10+c-'0';
    x*=a;
    return;
}

#define MAXN 100010

struct Leftist_Tree {
    int lc,rc,val,dis;
    bool operator < (const Leftist_Tree &q)const {
        return val<q.val;
    }
    bool operator == (const Leftist_Tree &q)const {
        return val==q.val;
    }
};

Leftist_Tree t[MAXN];

int fa[MAXN];

#define Lc t[x].lc
#define Rc t[x].rc

int Merge(int x,int y) {
    if(!x||!y) return x+y;
    if(t[y]<t[x]) swap(x,y);
    if(t[x]==t[y]&&x>y) swap(x,y);
    fa[Rc=Merge(Rc,y)]=x;
    if(t[Lc].dis<t[Rc].dis)
        swap(Lc,Rc);
    t[x].dis=t[Rc].dis+1;
    return x;
}

bool not_intree[MAXN];

void pop(int x) {
    not_intree[x]=true;
    fa[Lc]=fa[Rc]=0;
    Merge(Lc,Rc);
    return;
}

#undef Lc
#undef Rc

int Find(int x) {
    for(;fa[x];x=fa[x]);
    return x;
}

int main() {
    int n,m;
    input(n),input(m);
    for(int i=1;i<=n;i++)
        input(t[i].val);
    t[0].dis=1;
    for(int op,x,y;m;m--) {
        input(op);
        if(op==1) {
            input(x),input(y);
            if(x==y) continue;
            if(not_intree[x]||not_intree[y]) continue;
            Merge(Find(x),Find(y));
        } else {
            input(x);
            if(not_intree[x]) puts("-1");
            else {
                printf("%d\n",t[y=Find(x)].val),
                pop(y);
            }
        }
    }
    return 0;
}                   

这道题需要大根堆,在我的代码里,只需要改重载运算符就可以了

建议大家也这么写

这道题在 pop 的时候需要返回 pop 之后的根,而且不需要打 notintree 标记

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;

template<typename T>
void input(T &x) {
    x=0; T a=1;
    register char c=getchar();
    for(;c<48||c>57;c=getchar())
        if(c==45) a=-1;
    for(;c>=48&&c<=57;c=getchar())
        x=x*10+c-48;
    x*=a;
    return;
}

#define MAXN 100010

struct Leftist_Tree {
    int lc,rc,val,dis;
    bool operator < (const Leftist_Tree &q)const {
        return val>q.val;
    }
    bool operator == (const Leftist_Tree &q)const {
        return val==q.val;
    }
};

Leftist_Tree t[MAXN];

int fa[MAXN];

int Find(int x) {
    for(;fa[x];x=fa[x]);
    return x;
}

#define Lc t[x].lc
#define Rc t[x].rc

int Merge(int x,int y) {
    if(!x||!y) return x+y;
    if(t[y]<t[x]) swap(x,y);
    if(t[x]==t[y]&&x>y) swap(x,y);
    fa[Rc=Merge(Rc,y)]=x;
    if(t[Lc].dis<t[Rc].dis)
        swap(Lc,Rc);
    t[x].dis=t[Rc].dis+1;
    return x;
}

int pop(int x) {
    fa[Lc]=fa[Rc]=0;
    int ans=Merge(Lc,Rc);
    Lc=0,Rc=0;
    return ans;
}

#undef Lc
#undef Rc

int n;

void Clear() {
    for(int i=0;i<=n;i++) {
        fa[i]=0;
        t[i].lc=t[i].rc=t[i].dis=0;
    }
    t[0].dis=1;
    return;
}

int main() {
    while(~scanf("%d",&n)) {
        Clear();
        for(int i=1;i<=n;i++)
            input(t[i].val);
        int m;
        input(m);
        for(int x,y;m;m--) {
            input(x),input(y);
            x=Find(x),y=Find(y);
            if(x==y) puts("-1");
            else {
                t[x].val>>=1,t[y].val>>=1;
                int rt1=Merge(x,y),
                    rt2=Merge(pop(x),pop(y));
                printf("%d\n",t[Merge(rt1,rt2)].val);
            }
        }
    }
    return 0;
}

好像 HDU 也有这道题

难题

写不来的。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值