1. 程式人生 > 資訊 >不符合科學理論的引力波資料,揭示中子星雙星系統的形成祕密

不符合科學理論的引力波資料,揭示中子星雙星系統的形成祕密

目錄

平衡樹

用不同平衡樹解決模板題

Treap

簡介

在維持二叉查詢樹性質的基礎上,通過改變二叉查詢樹的形態,使得樹上每個節點的左右子樹大小達到平衡。這樣能使整棵樹的深度維持在\(O(logn)\)級別。

變數名

  • \(l,r\) 左右子節點在陣列中的下標
  • \(val\) 真實權值
  • \(dat\) 隨機權值
  • \(cnt\) 權值次數(即已經存在的的數值總共出現的次數,可以理解為副本數。這樣在插入時就可以直接在原權值的基礎上計數cnt加1,避免了無意義重複插入;而在刪除時直接將cnt-1,當cnt減到0的時候刪除該節點。增加一個cnt域的作用主要體現在可以比較容易地處理重複權值的問題)
  • \(sz\) 子樹大小
struct Treap {
    int l,r,val,dat,cnt,sz;
}a[maxn];

基本操作

Treap的核心操作:旋轉

旋轉又分為左旋和右旋。看張圖感性理解:

  1. 左旋

    \(zag(x)\)\(x\)的右子節點繞著\(x\)向左旋轉。結合上圖可以以模擬的想法寫出程式碼:

    inline void zag(int &x) {
        int y=a[x].r;
        a[x].r=a[y].l;
        a[y].l=x;
        x=y; //注意此處的x是引用,因為要使原來的x的值跟著改變。
    }
    
  2. 右旋

    \(zig(y)\)

    \(y\)的左子節點繞著\(y\)向右旋轉。

    inline void zig(int &y) {
        int x=a[y].l;
        a[y].l=a[x].r;
        a[x].r=y;
        y=x;
    }
    

總結一點小規律:是?旋,就是把指定節點的?的反方向的子樹朝?旋轉。

插入

Treap,顧名思義,有著Heap(堆)的性質,這一性質的維護體現在它的隨機操作上。

在插入每個新節點時,給該節點隨機生成一個額外的權值。然後從下而上依次檢查,當某個節點不滿足大根堆的性質時,就執行單旋操作,使其與其父節點的相對關係對換。細節看程式碼:

inline void Getnew(int val) { //得到一個新節點,直接插入在陣列尾端。val為原來的真實權值。
    a[++tot].val=val;
    a[tot].dat=rand();
    a[tot].cnt=a[tot].sz=1;
    return tot;
}
inline void Update(int p) { //更新子樹大小。
    a[p].sz=a[a[p].l].sz+a[a[p].r].sz+a[p].cnt;
}
inline void Insert(int &p,int val) {
    if (!p) {
        p=Getnew(val);
        return;
    }
    if (val==a[p].val) {
        ++a[p].cnt; //即上文提到過的cnt域的作用,已經出現過的重複權值計數+1。
        Update(p);
        return;
    }
    if (val<a[p].val) {
        Insert(a[p].l,val);
        if (a[p].dat<a[a[p].l].dat) zig(p); //不滿足堆性質,左子節點隨機權值大於父親隨機權值,執行右旋操作,把左子節點轉到原來父親的位置來。
    }
    else {
        Insert(a[p].r,val);
        if (a[p].dat<a[a[p].r].dat) zag(p); //同上,反之。
    }
    Update(p); //最後別忘記還要來一次更新。
}
刪除

旋轉操作的方便之處或許也在刪除這個操作上體現得淋漓盡致。對於指定要刪除的節點,直接把它一路向下旋轉成葉節點,最後直接從陣列中刪去即可。

inline void Delete(int &p,int val) {
    if (!p) return;
    if (val==a[p].val) { //檢索到val。
        if (a[p].cnt>1) {
            --a[p].cnt; //也是上文提到過的cnt域的作用,如果這個權值的計數仍舊大於1,說明這個權值為此數值的元素個數大於1,直接減去一個計數即可。
            Update(p);
            return;
        }
        if (a[p].l || a[p].r) { //不是葉節點,向下旋轉。
            if (a[p].r==0 || a[a[p].l].dat>a[a[p].r].dat) //當右子樹為空或左子節點的隨機權值大於右子節點,執行右旋操作,將原指定刪除的節點轉到其右節點處,再刪除其即可。
                zig(p), Delete(a[p].r,val);
            else 
                zag(p), Delete(a[p].l,val); //同上,反之。
            Update(p);
        }
        else p=0; //直接刪除葉子節點
        return;
    }
    val<a[p].val?Delete(a[p].l,val):Delete(a[p].r,val);//未檢索到val,繼續查詢。
    Update(p);
}

P3369模板 Treap做法

Splay

簡介

二叉查詢樹的一種。最大的特點是旋轉,通過不斷將某個節點旋轉到根節點,來使整棵樹始終保持二叉查詢樹的性質,此之謂保持平衡。

變數名:

  • \(rt\) 根節點
  • \(tot\) 節點總個數
  • \(fa[i]\) 節點\(i\)的父親節點
  • \(ch[i][0/1]\) 節點\(i\)的左/右兒子
  • \(val[i]\) 節點\(i\)的權值
  • \(cnt[i]\) 節點\(i\)的權值次數
  • \(sz[i]\) 以節點\(i\)為根的子樹大小

基本操作

判斷是左兒子還是右兒子

0為左兒子,1為右兒子。

#define get(p) (p==ch[fa[p][1]])
核心操作:旋轉

將p旋轉上移一個位置。

inline void rot(int p) {
    int x=fa[p],y=fa[x],u=get(p),v=get(x);
    fa[ch[p][u^1]]=x;  ch[x][u]=ch[p][u^1]; //舉個例子,若指定節點p是其父親的右兒子,則把p的左兒子的父親賦為p的父親,與此同時,p父親的右兒子賦為p原來的左兒子(其實這是不是很像Treap的左旋啊,相當於對p的父親進行左旋操作)。左右反之亦然(不管是哪個方向旋轉,都是把當前節點向上旋轉罷了)。
    fa[x]=p; ch[p][u^1]=x; upd(x); upd(p);
    fa[p]=y; if (fa[p]) ch[y][v]=p; //將p的父親賦為p原來的父親的父親(就是說它的爺爺(霧),然後把它的爺爺的孩子設成它就完事了。
}

其實程式碼真的很簡短啊,如神的異或操作遠遠勝過我匱乏的語言與其蒼白無力的詮釋。我的意思是好背就行。

讓我們來一張圖感受一下以上程式碼中以p是其父親右兒子的舉例:

靈活運用rot的splay操作

顧名思義,splay是splay最重要的操作(廢話)。

將p旋轉為g的兒子(若g為0則旋轉為根)。

inline void Splay(int p,int g) {
    while (fa[p]!=g) {
        int x=fa[p],y=fa[x];
        if (y==g) rot(p); //若p的爺爺就是目標g,單旋即可。具體可看上圖。
        else rot(get(p)==get(x)?x:p),rot(p); //對於父子共線的情況,即都是在一個方向上的,先將父親旋轉,再將自身旋轉;若不共線,將自身旋轉兩次即可。具體看下圖。
    }
    if (!g) rt=p;
}

不共線情況:

共線情況:

刪除

這個操作比較特別且富有Splay的特色,很巧妙。

首先把要刪除的點通過Splay操作搞到根節點。這個可以借用查詢排名函式,也就是可以把第一個大於等於當前點的數弄到根節點去。

若這個節點的計數大於1,把計數並子樹大小減去1即可。

若它不存在子節點,直接刪除即可。

若有一個子樹為空,讓那個不為空的子節點成為新的根,然後刪去指定節點即可。

如果以上這些特殊情況都不滿足,啟用PlanB!借用查詢前驅函式,把第一個小於等於v的數搞到根節點去。動用您優秀的想象能力,顯然,當前節點就變成了根的右兒子,且當前節點一定沒有左兒子。為什麼?因為比它小的數都是根節點及其左子樹。那麼就可以直接刪去了。再把刪去的節點的右兒子賦為根的右兒子即可,容易發現這樣完全不會破壞二叉查詢樹的性質。

以上都是廢話,細節看程式碼:

inline void Delete(int v) {
	GetRank(v); //借用查詢排名函式,將第一個大於等於v的數splay到根節點。
	if (v!=val[rt]) return; //v不存在 
	if (cnt[rt]>1) {--cnt[rt]; --sz[rt]; return;}
	int p=rt;
	if (!ch[p][0] && !ch[p][1]) {rt=0; clr(p); return;} //不存在子節點 
	if (!ch[p][0] || !ch[p][1]) {rt=ch[p][0]+ch[p][1]; fa[rt]=0; clr(p); return;}
	GetPre(v); //借用查詢前驅函式,把第一個小於等於v的數splay到根節點,那麼當前要刪除的數v必然是根的右子節點,且v一定沒有左子節點。這是我們直接刪除p,再把p原來的右子節點賦給根的右子節點。 
	fa[ch[p][1]]=rt; ch[rt][1]=ch[p][1]; clr(p); --sz[rt]; return; 
}
其他操作

大體上沒什麼區別,細節還是挺需要注意的。

一點不同:所有操作結束後,需要把當前操作的點splay到根節點。

P3369模板 Splay做法

應用

P3391文藝平衡樹

Splay技能:實現區間翻轉。

程式碼

FHQ Treap

簡介

無旋式Treap。顧名思義,不需要旋轉操作,就可以使Treap達到平衡。可以做到任何Splay和普通Treap能做的操作。

變數名

同一般Treap。

基本操作

不得不提使FHQ區別於其他平衡樹的維護操作:分裂與合併。

分裂

顧名思義,把一顆Treap分裂成兩顆,此過程用遞迴實現。

分裂還要分為兩種,是按什麼來分裂?

按權值分裂

權值小於等於v的節點分到左樹x,並在左樹中對右兒子建立虛擬節點且繼續分裂右子樹;大於v的分到右樹y,並在右樹中對左兒子建立虛擬節點且繼續分裂左子樹。

#define upd(p) a[p].sz=a[a[p].l].sz+a[a[p].r].sz+1
inline void split(int p,int v,int &x,int &y) {
	if (!p) {x=y=0; return;}
	if (a[p].val<=v) x=p, split(a[p].r,v,a[p].r,y);
	else y=p,split(a[p].l,v,x,a[p].l);
	upd(p);
}
按排名分裂

排名<=k的分到左樹x,大於k的分到右樹y。

inline void split(int p,int k,int &x,int &y) {
    if (!p) {x=y=0; return;}
    if (sz[a[p].l]+1<=k) x=p,split(a[p].r,k-sz[a[p].l],a[p].r,y);
    else y=p,split(a[p].l,k,x,a[p].l);
}
合併

依舊顧名思義,把兩顆Treap合成一個,也是遞迴實現。

過程中以隨機權值做條件來判斷是左樹還是右樹。返回合併後的根。

注:我們預設左樹x的權值都小於右樹y。

inline void split(int p,int k,int &x,int &y) {
    if (!p) {x=y=0; return;}
    if (sz[a[p].l]+1<=k) x=p,split(a[p].r,k-sz[a[p].l],a[p].r,y);
    else y=p,split(a[p].l,k,x,a[p].l);
}
其他操作

比較平常,但是不得不說FHQ是最好寫的一種平衡樹了。從碼量上來看。

P3369模板 FHQ解法

參考資料

  1. 平衡樹學習筆記 by xht37
  2. 學習筆記:平衡樹 by cyh_toby
  3. 《演算法競賽進階指南》