1. 程式人生 > 其它 >嶗山x牛x預備隊第一次直面深度學習

嶗山x牛x預備隊第一次直面深度學習

左偏樹

左偏樹是一種可並堆,具有堆的性質,且可以快速合併。

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;
}