左偏樹
咕咕咕
好久沒寫博客了,之前堅持三天就鴿了證明自己一個月啥都沒學,以後還是要寫的。
以下內容參考了大佬的博客和luoguP3377的題解區,%%%。
堆,這個肯定都知道。“不就是優先隊列嗎”,本來一直保持着這樣的想法,直到前幾天幫室友驗一道給數據結構基礎課出的題時,突然發現自己連個堆都實現不來(這就是不聽課的後果)有一說一真的菜b,於是爲了偷懶(?學了一個神奇的數據結構——左偏樹。
首先,我們知道堆的兩個性質:
[1].堆是一棵完全二叉樹
[2].堆上各結點的值從根往下具有單調性(此處指非嚴格單調性即可以相等,下同)
由此,我們可以得到一些結論:
[3].設結點數量爲的堆的最大深度爲,由[1]得,深度最大爲
[4].由[2]&[3]得,要將根節點調整爲最值,最多從根開始遍歷一整條鏈。因此往堆中增刪根結點,調整堆的時間複雜度是
所以,我們就獲得了一個時間動態求最值的數據結構。
但是,更進一步,如果要求合併兩個大頂堆,該怎麼做呢?
這題我會暴力啊 顯然,如果操作數和差不多的話,暴力就會導致的整體複雜度,這時就應該換個數據結構了。
左偏樹(Leftist Tree/Leftist Heap)
在說左偏樹之前,我們需要定義一個變量,其定義爲結點和最近空結點的距離,當爲空結點時,正是由於的存在,我們才能保證左偏樹的效率。
首先,仿照堆,我們也說說左偏樹的性質:
[1].對於左偏樹上的所有結點,都存在,其中分別指當前點的左右子結點
[2].左偏樹上各結點的值和從根往下具有單調性
類比上面對堆的描述,我們也可以得到一些結論:
[3].由[1]&[2]得,對於每一個結點總存在,其中爲的右孩子。
[4].設結點數量爲的左偏樹的**根節點的**爲,由[1]&[3]得,最大爲
在上面的性質和結論的基礎上,我們就可以進行核心的合併操作了:
設有兩棵根節點爲最小值的左偏樹和,表示結點的值,表示結點的右孩子,假設。
由[3]&[4],我們可以知道一棵個結點的左偏樹最多有條的從根一直往右的邊(下稱爲右向邊),從而可以在的時間內在右向邊上找到一個,所以可以不影響左偏性地將插入到和之間,隨後從開始往更新,同時判斷是否存在不滿足[3]的情況,若存在則交換當前結點的左右子樹。
於是就愉快地結束啦!什麼?你問怎麼刪除堆頂?
沒問題呀,左偏樹的子樹都具有左偏性質,去掉堆頂就相當於把原來的根的兩棵子樹重新合併,直接把之前的根重置後merge就好了。
(圖片來源:洛谷)
那就趁熱來一……道例題吧!
題意大致是,初始有個互不認識的猴子,每個猴子都有一個戰鬥力,他們會打場架,每場架的雙方爲和,雙方會找各自的朋友中戰鬥力最高的那個(可能是他自己)來打架,之後雙方打了架的猴子元氣大傷戰力減半,然後兩方猴成爲了朋友(不打不相識?,對於每個和,若雙方已經爲朋友則輸出,否則輸出打完架之後,這一幫猴子中最高的戰鬥力。
這就是左偏樹模板題了(雖然二項堆/斐波那契堆/配對堆好像也能做,不過我太菜了還不會),初始有個大頂堆,每次打架取出兩堆頂,減半後放入原堆並把兩個堆合二爲一併用並查集維護朋友關係。左偏樹寫起來就很簡單,來回merge一下就ac啦,單組數據時間複雜度。
AC代碼:
#include<bits/stdc++.h>
#define N 100010
using namespace std;
int n;
struct LeftTree
{
int d,v,l,r,f;//分別代表dis,value,leftson,rightson,father
}lt[N];
void init(int n)//多組數據初始化
{
for(int i=1;i<=n;i++)lt[i].f=i,lt[i].l=lt[i].r=lt[i].v=lt[i].d=0;
}
int find(int x)//並查集
{
return (x^lt[x].f)?lt[x].f=find(lt[x].f):x;
}
int merge(int x,int y,int rt=0)//返回新堆的根
{
if(!x||!y){lt[x+y].f=rt?rt:x+y;return x+y;}
if(lt[x].v<lt[y].v)swap(x,y);//大頂堆
lt[x].f=rt?rt:rt=x;//更新父結點(根)
lt[x].r=merge(lt[x].r,y,rt);//遞歸右子樹
if(lt[lt[x].l].d<lt[lt[x].r].d)swap(lt[x].l,lt[x].r);//調整左右結點dis
lt[x].d=lt[lt[x].r].d+1;
return x;
}
int m,x,y;
int main()
{
ios::sync_with_stdio(false);
while(cin>>n)
{
init(n);
for(int i=1;i<=n;i++)cin>>lt[i].v;
cin>>m;
while(m--)
{
cin>>x>>y;
int a=find(x),b=find(y),c,d,e;
if(a==b)
{
cout<<-1<<"\n";
continue;
}
lt[a].v>>=1;lt[b].v>>=1;
//注意取出堆頂之後要將左右孩子置空,不然會出現兩結點互爲父親的情況
c=merge(lt[a].l,lt[a].r);lt[a].l=lt[a].r=0;
d=merge(lt[b].l,lt[b].r);lt[b].l=lt[b].r=0;
a=merge(a,c);b=merge(b,d);//放回減半的結點
e=merge(a,b);//合併兩棵樹
cout<<lt[e].v<<'\n';
}
}
}
P.S:一直有個疑問,樹上的“結點”和“節點”應該用哪個,還是說哪個都可以嗎直接說node不香嗎