不符合科學理論的引力波資料,揭示中子星雙星系統的形成祕密
平衡樹
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的核心操作:旋轉
旋轉又分為左旋和右旋。看張圖感性理解:
-
左旋
\(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的值跟著改變。 }
-
右旋
\(zig(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);
}
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到根節點。
應用
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是最好寫的一種平衡樹了。從碼量上來看。
參考資料
- 平衡樹學習筆記 by xht37
- 學習筆記:平衡樹 by cyh_toby
- 《演算法競賽進階指南》