淺談左偏樹在OI中的應用
Preface
可並堆,一個聽起來很NB的數據結構,實際上比一般的堆就多了一個合並的操作。
考慮一般的堆合並時,當我們合並時只能暴力把一個堆裏的元素一個一個插入另一個堆裏,這樣復雜度將達到\(\log(|A|)+\log(|B|)\),極限數據下顯然是要T爆的。
所以我們考慮使用一種性價比最高的可並堆——左偏樹,它的思想以及代碼都挺簡單而且效率也不錯。
學習和參考自這裏
What is Leftist Tree
左偏樹,顧名思義就是像左偏的樹,但是這樣抽象的表述肯定是不符合我們學OI的人的背板子嚴謹的態度的。
我們給出一些定義:
- 外節點:當且僅當這個節點的左子樹和右子樹中的一個是空節點,註意外節點不是葉子節點
- 距離(或者是高度?):對於左偏樹中的一個節點\(x\),到它的子節點中,離它最近的一個外結點經過的邊數稱為它的距離,記為\(dist_x\)。特別地,外結點的距離為0,空節點(null)的距離為\(-1\)。
然後跟著這個定義我們可以得出一些性質:
- 堆性質:對於左偏樹中的一個非葉節點應滿足堆的性質。如果是大根堆,應滿足任意非葉節點的值大於左右孩子(如果有的話)的值。即\(val_x\ge val_{lson(x)},val_{rson(x)}\)
- 左偏性質:對於左偏樹中的任意節點滿足它的左子樹(如果有的話)的距離大於等於右子樹(如果有的話)的距離。即\(dist_{lson(x)}\ge dist_{rson(x)}\)
- 傳遞性?:左偏樹任意節點的左右兒子(如果有的話)都是一棵左偏樹
廢話
由這幾條性質可以發現左偏樹是具有左偏性質的堆有序二叉樹。
同時還有一條不可忽視的終於引理:左偏樹中的節點的距離總是滿足\(dist_x=dist_{rsom(x)}+1\)
證明:同時由距離的定義以及左偏性質即可得出。
The operator of Leftist Tree
BB了這麽就性質啥的抽象東西,是時候講點真正有用的東西了。
Merge
可並堆的基本操作(也是必備操作)自然是快速地完成合並了,同時合並也是左偏樹的靈魂部分,只要說掌握了合並的話就可以直接拿著左偏樹大力切題了。
以下假定大根堆的情況,我們假定要合並的兩個左偏樹(註意單個節點也是左偏樹
我們首先維護堆的性質,令\(val_x>val_y\),即當值不滿足的時候\(\operatorname{swap}(x,y)\)
那麽我們發現這時候就要把\(y\)插入\(x\)的子樹中了,換句話說,就是要把\(y\)和\(x\)的子樹合並。
那麽和誰合並呢?考慮我們辛辛苦苦維護的左偏性質,由於右邊的鏈長小於等於左邊的,所以為了保證復雜度肯定是直接和\(dist\)小的合並了,於是我們合並\(rson(x)\)和\(y\)
但是插入後右子樹的\(dist\)可能會大於左邊了這樣就變右偏樹了,我們肯定是不允許的,於是我們判斷一下這種情況,如果有就直接交換左右子樹(註意直接交換編號即可)即可。
那麽什麽時候結束呢,當然是當\(x\)或\(y\)中一者為空了啦。所以我們可以比較輕松的得到合並的代碼(返回合並的堆的堆頂編號):
inline int merge(int x,int y)
{
if (!x||!y) return x+y; if (c[x]<c[y]) swap(x,y);
rc(x)=merge(rc(x),y); if (node[lc(x)].dis<node[rc(x)].dis) swap(lc(x),rc(x));
node[x].dis=node[rc(x)].dis+1; return x;
}
這裏放一張Luogu上找到的動圖可以更加生動地理解下:
復雜度的話類似啟發式的思想發現是\(O(\log)\)級別的。
push
push的本質就是把一個只有一個節點的左偏樹於一顆左偏樹合並,因此直接用merge即可。
top
這個直接返回根節點的權值即可。
pop
刪除根節點的話可以考慮合並根節點的兩個子樹,代碼
inline void remove(int &x)
{
x=merge(lc(x),rc(x));
}
板子題:Luogu P3377 【模板】左偏樹(可並堆)
這個我們維護小根對的時候再維護一個父節點的信息即可查詢兩個數是否在同一顆左偏樹裏了。
CODE
#include<cstdio>
#include<cctype>
using namespace std;
const int N=100005;
struct Leftist_Tree
{
int ch[2],val,dis,fa;
}node[N];
int n,m,x,y,opt;
inline char tc(void)
{
static char fl[100000],*A=fl,*B=fl;
return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
x=0; char ch; while (!isdigit(ch=tc()));
while (x=(x<<3)+(x<<1)+ch-‘0‘,isdigit(ch=tc()));
}
inline void write(int x)
{
if (x>9) write(x/10);
putchar(x%10+‘0‘);
}
inline void swap(int &a,int &b)
{
int t=a; a=b; b=t;
}
inline int merge(int x,int y)
{
if (!x||!y) return x+y;
if (node[x].val>node[y].val||(node[x].val==node[y].val&&x>y)) swap(x,y);
node[x].ch[1]=merge(node[x].ch[1],y); node[node[x].ch[1]].fa=x;
if (node[node[x].ch[0]].dis<node[node[x].ch[1]].dis) swap(node[x].ch[0],node[x].ch[1]);
node[x].dis=node[node[x].ch[1]].dis+1; return x;
}
inline void remove(int x)
{
node[x].val=-1; node[node[x].ch[0]].fa=node[node[x].ch[1]].fa=0;
merge(node[x].ch[0],node[x].ch[1]);
}
inline int getfather(int x)
{
while (node[x].fa) x=node[x].fa; return x;
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
register int i; read(n); read(m); node[0].dis=-1;
for (i=1;i<=n;++i) read(node[i].val);
while (m--)
{
read(opt); read(x); if (opt^2)
{
read(y); int fx=getfather(x),fy=getfather(y);
if (~node[fx].val&&~node[fy].val&&fx!=fy) merge(fx,fy);
} else
{
int fx=getfather(x); if (!(~node[fx].val)) puts("-1"); else
write(node[fx].val),putchar(‘\n‘),remove(fx);
}
}
return 0;
}
例題
- Luogu P1552 [APIO2012]派遣可並堆好題。考慮以每個點為領導者計算答案那麽可選的點都在這顆子樹中,那麽考慮用的人最多我們就貪心的把大的扔掉,回溯時合並兩個堆即可。左偏樹處理。
- BZOJ 1367: [Baltic2004]sequence 可並堆好題,一眼看不出。考慮先求不降序列的情況,發現可以對原序列的所有不升序列進行分割,這樣必然是取中位數(反證法)。然後用左偏樹統計中位數並支持合並即可。
Postscript
可並堆算是介於TG和省選直接的尷尬內容的吧,往年NOIp的話不見出現過。
左偏樹雖說在效率上次於配對堆,斐波那契堆這些神仙數據結構,但是它簡單的思想以及碼量都是很良心的。
還是稍微要掌握一下的吧。
淺談左偏樹在OI中的應用