曝華為 MatePad 11 平板電腦售價 3000 元左右:7 月釋出,搭載驍龍 865、鴻蒙 HarmonyOS 2
左偏樹,是一種可以在 \(\Theta(\log_2 n)\) 複雜度內合併的一種資料結構。
它是以二叉樹的形態來維護堆的性質,由此也延伸出很多別的操作。
接下來本文所述堆及其操作均預設為小根堆。
\[\]一些約定
-
\(v_i\) 節點 \(i\) 的權值。
-
\(l_i,r_i\) 節點 \(i\) 的左右兒子。
-
\(d_i\) 節點 \(i\) 到其最近外節點的距離。
-
\(f_i\) 節點 \(i\) 的父節點。
什麼是外節點:一個左兒子或右兒子是空節點的節點。
\[\]左偏樹的性質
堆性質
作為堆式資料結構顯然有堆的性質,即對任意一個樹中的點 \(i\),\(v_i<v_{l_i},v_i<v_{r_i}\)
但是任意一個節點左右兒子的大小並不確定,這也不在我們的維護範圍之內。
左偏性質
左偏,就是對於任意一個樹中的點 \(i\),\(d_{l_i}>d_{r_i}\)。
另外的,對於任意一個樹中的點 \(i\),\(d_i=d_{r_i}+1\)。
\[\]左偏樹的操作
合併操作
左偏樹最核心的操作,其餘的操作也均以這個操作為基礎。
假設我們要合併分別以 \(a,b\) 為根節點的兩棵左偏樹。
首先,若其中一棵樹的根為空節點,則另一棵樹的根成為新樹的根。
若兩個樹根均存在,那麼權值更小的必定成為新的樹根。
這裡我們假設 \(v_a<v_b\),即 \(a\) 為新樹的根。為了避免分類討論,當 \(v_a>v_b\)
此時我們再令 \(b\) 與 \(a\) 的右兒子進行合併,重新比較其大小。如此不斷比較下去直到遇到空節點或葉節點為止。
這樣遞迴返回每次合併的根節點的值,重置此樹的相關節點。
另外,由於是對右兒子進行合併,所以可能合併後的樹不再滿足左偏性質。此時我們交換左右子樹。
我們要時刻保證對於任意一個樹中的節點 \(i\),\(d_i=d_{r_i}+1\),所以我們重新規定一下右兒子的距離即可。
實現也比較容易。
int merge(int x,int y){ if(!x||!y) return x+y; if(v[y]<v[x]) swap(x,y); rs=merge(rs,y); if(dis[ls]<dis[rs])swap(ls,rs); dis[x]=dis[rs]+1; return x; }
還有一種寫法是隨機合併,引入隨機數來判斷是否交換左右節點。
這樣做可能會使樹不滿足左偏性質,但是省去了距離的相關計算,且複雜度不變。
取最值操作
由於左偏樹的堆性質,它的最值肯定在堆頂。那麼就直接不斷跳父節點就行了。
但是我們發現暴跳太慢了。我們可以藉助並查集對其路徑壓縮。
對於同一棵左偏樹中的點用並查集連線到一起,並欽定其祖先節點為堆頂。
於是就可以在 \(\Theta{\log_2 n}\) 的時間內快速找出最值了。
刪除操作
刪除操作只針對堆頂節點來說。
刪除節點需要重置其資訊(左右兒子和距離)。
另外,刪除此節點之後,其真正的左右兒子並沒有被刪除,但是現在他們都沒有了父親(?),所以我們需要合併這兩個點。
fa[lson[x]]=fa[rson[x]]=fa[x]=merge(lson[x],rson[x]);
注意這個節點本身的父親也要指向合併後的根節點。
因為路徑壓縮後刪除此節點,此節點的父親指向的是自己,若不更改可能會導致後面出各種問題。
插入節點
等同於將其與一棵大小為一的堆合併。
此時要保證插入的這個點的各個資訊已經處理完善。
全樹加/減/乘一個值
維護懶標記,從根節點開始不斷向下穿即可。具體是在刪除/合併訪問兒子時下傳。
由於只能單個傳,所以速度較慢。
當然不止這三種操作,可以打標記且不改變相對大小的操作都可以。
\[\]例題
Monkey King
給定初始的 \(n\) 個大小為一的堆及其權值,再給定 \(m\) 次詢問。
每次詢問x y
,表示將 \(x\) 與 \(y\) 所在的堆的堆頂權值變為原來的一半後合併。
對於每次操作,輸出新堆的堆頂權值。
\(n,m\le 100000\)
一開始的思路是,每次新建兩個權值分別為兩個堆頂權值一半的點,先將這兩個點與這兩個堆合併,再刪除堆頂,最後合併兩個堆。
至於權值方面,由於我懶得寫大根堆,於是就將權值的相反數插入堆中建小根堆。
注意奇數的整除是下取整,所以對於奇數 \(a\),要將 a>>=1
寫成 a=-((-a)>>1)
。
計算下來若每次新建兩個點,那麼總共就是 \(2\times m\) 個,加上原來的應該不超過 \(300000\) 個點。
但不知道為什麼總是 MLE,至今沒找到原因(
所以我換了寫法,不再新建節點,而是先刪除兩個堆頂,再在每個堆中加入新點,只不過點的編號還是原來堆頂的。
最後合併兩個堆就做完了。
程式碼和板子的差不多。
#include<queue>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 100100
#define INF 0x3f3f3f3f
//#define int long long
using namespace std;
int n,m,cnt;
int read(){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9')s=(s<<1)+(s<<3)+ch-'0',ch=getchar();
return s*w;
}
namespace LIT{
#define ls lson[x]
#define rs rson[x]
int fa[maxn],dis[maxn];
int lson[maxn],rson[maxn];
struct node{
int pos,val;
bool operator < (const node &b) const{
return val^b.val?val<b.val:pos<b.pos;
}
}v[maxn];
int findf(int x){
return fa[x]==x?x:fa[x]=findf(fa[x]);
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(v[y]<v[x]) swap(x,y);
rs=merge(rs,y);
if(dis[ls]<dis[rs])swap(ls,rs);
dis[x]=dis[rs]+1;
return x;
}
}
using namespace LIT;
signed main(){
dis[0]=-1;
while(scanf("%d",&n)!=EOF){
for(int i=1;i<=n;i++){
lson[i]=rson[i]=dis[i]=0;
v[i].val=read()*-1;
v[i].pos=fa[i]=i;
}
m=read();cnt=n;
for(int i=1,x,y;i<=m;i++){
x=findf(read());y=findf(read());
if(x==y){printf("-1\n");continue;}
v[x].val=-((-v[x].val)>>1);
int rt1=merge(lson[x],rson[x]);
lson[x]=rson[x]=dis[x]=0;
int now1=fa[rt1]=fa[x]=merge(rt1,x);
v[y].val=-((-v[y].val)>>1);
int rt2=merge(lson[y],rson[y]);
lson[y]=rson[y]=dis[y]=0;
int now2=fa[rt2]=fa[y]=merge(rt2,y);
fa[now1]=fa[now2]=merge(now1,now2);
printf("%d\n",-v[fa[now1]].val);
}
}
return 0;
}