[StudyNotes] 左偏树

来源:互联网 发布:中国的未来在哪里 知乎 编辑:程序博客网 时间:2024/05/18 20:05

什么是左偏树?

左偏树(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=0lc,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].rcint 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 就可以了

可以用左偏树来做的题目

简单题

  • 洛谷 P3377 【模板】左偏树(可并堆)

直接贴代码了

#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 100010struct 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].rcint 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 Rcint 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;}                   
  • 洛谷 P1456 Monkey King

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

建议大家也这么写

这道题在 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 100010struct 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].rcint 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 Rcint 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 也有这道题

难题

  • bzoj 2333: [SCOI2011]棘手的操作

  • bzoj 2809: [Apio2012]dispatching

写不来的。。

原创粉丝点击