左偏樹(可並堆)

adsd45666發表於2024-08-08

左偏樹(可並堆)

定義

在這之前,我們先來闡述一些定義:

  1. 外節點:\(ls\)\(rs\) 為空的節點
  2. 距離:節點的距離 \(dist_x\) 定義為節點 \(x\) 到距 \(x\) 最近的外節點的距離,空節點的距離為 \(-1\)

其次是左偏樹的性質:

  1. 左偏性:即滿足 \(dist_{ls}>=dist_{rs}\)
  2. 堆性質:若滿足小根堆,則滿足 \(v_x<=v_{ls}\) , \(v_x<=v_{rs}\)

這引出了左偏樹的一些結論:

  1. 節點 \(x\) 的距離為 \(dist_{rs}+1\) (由於左偏性)
  2. 距離為 \(n\) 的左偏樹至少有 \(2^n-1\) 個節點,最少時,形態接近滿二叉樹
  3. \(n\) 的節點的左偏樹的根節點距離是 \(O(log_2n)\)

接下來是左偏樹支援的一些操作:

  1. 合併
  2. 插入給定值
  3. 求最小值
  4. 刪除最小值
  5. 求指定節點在左偏樹的根節點

詳解

一.合併

\(merge(x,y)\) 為合併以 \(x\) , \(y\) 為根節點的兩棵子樹,返回值為合併後的根節點。

先考慮堆的合併:

  1. \(v_x<=v_y\) ,則以 \(x\) 做為合併後的根節點。(若有 \(v_x>v_y\)\(swap(x,y)\) )
  2. \(y\)\(x\) 的一個兒子合併,合併後的根節點代替與 \(y\) 合併的兒子的位置,返回 \(x\)
  3. 重複以上操作,直到 \(x\)\(y\) 有一方是空節點。

然鵝,這樣合併的操作複雜度為 \(O(h)\) \(h\) 為樹高,當堆退化為鏈時,複雜度為 \(O(n)\) ,想要進一步最佳化,則要最佳化左偏樹。由於在左偏樹中,左兒子的距離大於右兒子的距離,所以每次選擇右兒子進行合併,則單次複雜度可以來到 \(O(long_2n)\) ;

但兩棵樹合併後可能破壞左偏樹的左偏性,故在每次合併後,判斷節點 \(x\) 是否符合 \(dist_{ls}>=dist_{rs}\) ,若不滿足則 \(swap(ls,rs)\) ,並維護 \(dist_x=dist_{rs}+1\) ,即可維護左偏樹的左偏性。

二.插入給定值

新建一個值等於插入值的點,將該節點與左偏樹合併即可。

三.求最小值

由於左偏樹的堆性質,所以左偏樹的最小值即為左偏樹的根節點的值。

四.刪除最小值

等價於刪除左偏樹的根節點,合併左右子樹即可。

五.求指定節點在左偏樹的根節點

可以記錄每個節點的父親節點,然後暴力跳父親節點。

int find(int x){
    if(rt[x]) return find(rt[x]);
    return x;
}

是不是非常熟悉,當然,可以使用路徑壓縮最佳化。

int find(int x){
    if(rt[x]) return rt[x]=find(fa[x]);
    return x;
}

如此,我們便需維護 \(rt\) 陣列。在合併兩個節點時,令:

rt[x]=rt[y]=merge(x,y);

在刪除左偏樹的最小值時

rt[ls[x]]=rt[rs[x]]=rt[x]=merge(ls[x],rs[x]);

因為 \(x\) 之前靠近根節點,在路徑壓縮時,\(rt\) 陣列有可能等於 \(x\) ,所以 \(rt[x]\) 也指向刪除後的根節點。

由於使用了路徑壓縮最佳化,導致 \(x\) 的樹形結構被破壞,若之後還需使用 \(x\) 則需重建一個同值節點。使用路徑壓縮最佳化後,可以在 \(O(log_2n)\) 的時間複雜度內找到一個點在左偏樹的根節點。

完整程式碼

#include <bits/stdc++.h>
#define seq(q, w, e) for (int q = w; q <= e; q++)
#define ll long long
using namespace std;
const int maxn = 1e5+10;
int m,n,x,y,op;
int ls[maxn],rs[maxn],dist[maxn],rt[maxn];
bool tf[maxn];
struct node{                                                         //點節點結構體
    int id,v;                                                        //編號,價值
    bool operator<(node x)const{return v==x.v?id<x.id:v<x.v;}
}v[maxn];
int find(int x){                                                     //尋找祖宗
    if(rt[x]==x)
        return x;
    return rt[x]=find(rt[x]);                                        //路徑壓縮
}
int merge(int x,int y){                                              //合併x,y
    if(!x||!y)                                                       //若這兩個節點中存在空節點
        return x+y;
    if(v[y]<v[x])                                                    //保證v[x]<v[y]
        swap(x,y);
    rs[x]=merge(rs[x],y);                                            //由於左偏性質,最優合右樹
    if(dist[ls[x]]<dist[rs[x]])                                      //維護左偏性質
        swap(ls[x],rs[x]);
    dist[x]=dist[rs[x]]+1;                                           //維護根節點距離
    return x;
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    dist[0]=-1;
    cin>>n>>m;
    seq(i,1,n){                                                      //初始化
        cin>>v[i].v;                                                 //輸入價值
        rt[i]=i;
        v[i].id=i;
    }
    while(m--){
        cin>>op>>x;
        if(op==1){
            cin>>y;
            if(tf[x]||tf[y]) continue;
            x=find(x);y=find(y);
            if(x!=y) rt[x]=rt[y]=merge(x,y);                         //若不在一棵樹上
        }
        else{
            if(tf[x]){
                cout<<"-1"<<"\n";
                continue;
            }
            x=find(x);
            cout<<v[x].v<<"\n";
            tf[x]=true;
            rt[ls[x]]=rt[rs[x]]=rt[x]=merge(ls[x],rs[x]);
            ls[x]=rs[x]=dist[x]=0;
        }
    }
    return 0;
}

相關文章