左偏树

左偏树

左偏树是一种可并堆,具有堆的性质,且可以快速合并。

dist 定义

对于一棵二叉树,我们定义外节点为左儿子或右儿子为空的节点,定义一个外节点的 \(dist\) 为 \(1\),一个不是外节点的 \(dist\) 为其到子树中最近的外节点的距离加一。空节点的 \(dist\) 为 \(0\)。

一棵有 \(n\) 个节点的二叉树,根的 \(dist\) 不超过 \(\left\lceil \log(n+1) \right\rceil\),这是因为一棵根的 \(dist\) 为 \(x\) 的二叉树至少有 \(x-1\) 层是满的,那么就至少有 \(2^x-1\) 个节点。注意这个性质是所有二叉树都具有的,并不是只有左偏树具有。

定义与性质

左偏树是一棵二叉树,它不仅具有堆的性质,并且是左偏的,即每个节点左儿子的 \(dist\) 都大于等于右儿子的 \(dist\)。

因此,左偏树的每个节点的 \(dist\) 都等于其右儿子的 \(dist\) 加一。

需要注意的是,左偏树的深度没有保证,一条向左的链也是左偏树。

核心操作:Merge

以下若无特殊说明,都指的是小根堆。

合并两个堆的时候,由于要满足堆的性质,先取值较小的节点作为合并之后的根节点,然后将这个根的左儿子左右合并之后根的左儿子,递归的合并其右儿子与另一个堆,作为合并后堆的右儿子。注意如果不符合左偏性质,就交换两个儿子。

代码:

inline int Merge(int a,int b){
        if(!a||!b) return a+b;
        if(p[b]<p[a]) swap(a,b);
        rs(a)=Merge(rs(a),b);
        if(p[rs(a)].dist>p[ls(a)].dist) swap(rs(a),ls(a));
        p[a].dist=p[rs(a)].dist+1;
        return a;
    }

由于左偏性质,每递归一层,其中 \(a,b\) 所代表的的节点的其中一个的 \(dist\) 会减 \(1\),所以合并两个大小为 \(n,m\) 的堆的复杂度为 \(O(\log n+\log m)\)。

有不用交换左右儿子的写法,但不建议学,个人感觉还是确定下来比较好。

其他操作

插入节点

把插入的节点视为堆,合并就可以。

删除根

合并左右子树就可以。

删除任意节点

线将左右儿子合并,然后直接从下向上更新 \(dist\),不满足左偏性质的时候交换左右儿子,当 \(dist\) 无需更新的时候结束递归。

  • 复杂度证明:

    我们令当前 pushup 的这个节点为 \(x\),其父亲为 \(y\),一个节点的初始 \(dist\) 为它在 pushup 前的 \(dist\)。我们先 pushup 一下删除的节点,然后从其父亲开始讨论复杂度。

    继续递归下去有两种情况:

    1. \(x\) 是 \(y\) 的右儿子,此时 \(y\) 的初始 \(dist\) 为 \(x\) 的初始 \(dist\) 加一。
    2. \(x\) 是 \(y\) 的左儿子,只有 \(y\) 的左右儿子初始 \(dist\) 相等的时候(此时左儿子 \(dist\) 减一会导致左右儿子交换)才会继续递归下去,因此 \(y\) 的初始 \(dist\) 仍然是 \(x\) 的初始 \(dist\) 加一。

所以我们得到,除了第一次 pushup(因为被删除节点的父亲的初始 \(dist\) 不一定等于被删除节点左右儿子合并后的初始 \(dist\) 加一),每递归一层 \(x\) 的初始 \(dist\) 就会加一,因此最多递归 \(\log n\) 层。

整个堆加上或减去一个值

直接在根上打标记,或者不改变相对大小的操作都可以。

打标记得时候注意每访问到就往下传就可以。

例题

左偏树模板题

注意因为我们要保证复杂度,所以我们不能暴力跳父亲来找根,我们用并查集来维护根。注意我们删除节点时并查集也要相对应的处理。

代码:

#include<bits/stdc++.h>
#include<iostream>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 500010
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

struct node{
    int fa,val,dist,l,r,id;
    inline node(){}
    inline node(int fa,int val,int dist,int l,int r,int id) :
        fa(fa),val(val),dist(dist),l(l),r(r),id(id) {}
    inline bool operator < (const node &b) const{
        if(val!=b.val) return val<b.val;
        return id<b.id;
    }
}p[N];

int fa[N];
inline int Find(int x){return x==fa[x]?x:fa[x]=Find(fa[x]);}

bool vis[N];

//我们规定,如果一个节点编号在并查集上所在集合的根为 k,那么该节点在左偏树上的根就是 k

struct LeftistTree{
    #define ls(a) p[a].l
    #define rs(a) p[a].r
    inline int Merge(int a,int b){
        if(!a||!b) return a+b;
        if(p[b]<p[a]) swap(a,b);
        rs(a)=Merge(rs(a),b);
        if(p[rs(a)].dist>p[ls(a)].dist) swap(rs(a),ls(a));
        p[a].dist=p[rs(a)].dist+1;
        return a;
    }
    inline void EasyMerge(int a,int b){
        if(vis[a]||vis[b]) return;
        int rta=Find(a),rtb=Find(b);
        if(rta==rtb) return;
        int nowrt=Merge(rta,rtb);
        if(nowrt==rtb) swap(rta,rtb);
        fa[rtb]=rta;
    }
    inline int Delete(int k){
        if(vis[k]) return -1;
        int rt=Find(k);vis[rt]=1;int ans=p[rt].val;
        int L=ls(rt),R=rs(rt);
        if(L==0||(p[R]<p[L]&&R!=0)) swap(L,R);
        Find(L);fa[rt]=L;fa[L]=L;
        Merge(ls(rt),rs(rt));return ans;
    }
}LT;

int n,m;

int main(){
    // freopen("my.in","r",stdin);
    // freopen("my.out","w",stdout);
    read(n);read(m);
    for(int i=1;i<=n;i++){
        int x;read(x);p[i]=node(0,x,1,0,0,i);
    }
    for(int i=1;i<=n;i++) fa[i]=i;
    for(int i=1;i<=m;i++){
        int op,a,b;
        read(op);read(a);
        if(op==1){
            read(b);
            LT.EasyMerge(a,b);
        }
        else printf("%d\n",LT.Delete(a));
    }
    return 0;
}
上一篇:11月17日总结


下一篇:环形链表python3(leetcode141)