Hile每日算法-4.23-左偏樹

左偏樹

咕咕咕

好久沒寫博客了,之前堅持三天就鴿了證明自己一個月啥都沒學,以後還是要寫的。

以下內容參考了大佬的博客luoguP3377的題解區,%%%。

堆,這個肯定都知道。“不就是優先隊列嗎”,本來一直保持着這樣的想法,直到前幾天幫室友驗一道給數據結構基礎課出的題時,突然發現自己連個堆都實現不來(這就是不聽課的後果)有一說一真的菜b,於是爲了偷懶(?學了一個神奇的數據結構——左偏樹。

首先,我們知道堆的兩個性質:

[1].堆是一棵完全二叉樹

[2].堆上各結點的值從根往下具有單調性(此處指非嚴格單調性即可以相等,下同)

由此,我們可以得到一些結論:

[3].設結點數量爲nn的堆的最大深度dd,由[1]得2d2log2n+12^d\le2^{log_2n+1},深度最大爲log2n+1log_2n+1

[4].由[2]&[3]得,要將根節點調整爲最值,最多從根開始遍歷一整條鏈。因此往堆中增刪根結點,調整堆的時間複雜度是O(logn)O(logn)

所以,我們就獲得了一個O(logn)O(logn)時間動態求最值的數據結構。

但是,更進一步,如果要求合併兩個大頂堆,該怎麼做呢?

這題我會暴力啊 顯然,如果操作數和nn差不多的話,暴力就會導致O(n2logn)O(n^2logn)的整體複雜度,這時就應該換個數據結構了。

左偏樹(Leftist Tree/Leftist Heap)

在說左偏樹之前,我們需要定義一個變量disidis_i,其定義爲ii結點和最近空結點的距離1-1,當ii爲空結點時disi=1dis_i=-1,正是由於disdis的存在,我們才能保證左偏樹的效率。

首先,仿照堆,我們也說說左偏樹的性質:

[1].對於左偏樹上的所有結點,都存在dislsdisrsdis_{ls}\ge dis_{rs},其中ls,rsls,rs分別指當前點的左右子結點

[2].左偏樹上各結點的值和disdis從根往下具有單調性

類比上面對堆的描述,我們也可以得到一些結論:

[3].由[1]&[2]得,對於每一個結點總存在disi=disrs+1dis_i=dis_{rs}+1,其中rsrsii的右孩子。

[4].設結點數量爲nn的左偏樹的**根節點的disdis**爲dd,由[1]&[3]得2d+1n+12^{d+1}\le n+1dd最大爲log2(n+1)1log_2(n+1)-1

在上面的性質和結論的基礎上,我們就可以進行核心的合併操作了:

設有兩棵根節點爲最小值的左偏樹aabbviv_i表示ii結點的值,rir_i表示ii結點的右孩子,假設vavbv_a\le v_b

由[3]&[4],我們可以知道一棵nn個結點的左偏樹最多有lognlogn條的從根一直往右的邊(下稱爲右向邊),從而可以在O(logn)O(logn)的時間內在右向邊上找到一個vavivbvriv_a\le v_i\le v_b\le v_{r_i},所以可以不影響左偏性地將bb插入到iirir_i之間,隨後從bb開始往aa更新disdis,同時判斷是否存在不滿足[3]的情況,若存在則交換當前結點的左右子樹。

於是就愉快地結束啦!什麼?你問怎麼刪除堆頂?

沒問題呀,左偏樹的子樹都具有左偏性質,去掉堆頂就相當於把原來的根的兩棵子樹重新合併,直接把之前的根重置後merge就好了。

img

(圖片來源:洛谷)

那就趁熱來一……道例題吧!

hdu1512 Monkey King

題意大致是,初始有nn個互不認識的猴子,每個猴子都有一個戰鬥力,他們會打mm場架,每場架的雙方爲aabb,雙方會找各自的朋友中戰鬥力最高的那個(可能是他自己)來打架,之後雙方打了架的猴子元氣大傷戰力減半,然後兩方猴成爲了朋友(不打不相識?,對於每個aabb,若雙方已經爲朋友則輸出1-1,否則輸出打完架之後,這一幫猴子中最高的戰鬥力。

這就是左偏樹模板題了(雖然二項堆/斐波那契堆/配對堆好像也能做,不過我太菜了還不會),初始有nn個大頂堆,每次打架取出兩堆頂,減半後放入原堆並把兩個堆合二爲一併用並查集維護朋友關係。左偏樹寫起來就很簡單,來回merge一下就ac啦,單組數據時間複雜度O(mlogn)O(mlogn)

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不香嗎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章