樹堆,在數據結構中也稱Treap,是指有一個隨機附加域滿足堆的性質的二叉搜索樹,其結構相當於以隨機數據插入的二叉搜索樹。其基本操作的期望時間復雜度為O(logn)。相對於其他的平衡二叉搜索樹,Treap的特點是實現簡單,且能基本實現隨機平衡的結構。
在深入了解Treap之前,我們先來了解一下BST。
BST(Binary-search tree),即二分搜索樹,是一棵二叉樹,且滿足性質:若每個節點都有一個key值,則對於每個根節點,均滿足key[leftson]<key[root]<key[rightson]。換句話說,即滿足樹的先序遍歷等於樹的中序遍歷。如下圖即為一顆BST:
其特點可以幫助我們快速查找樹中的某些元素。舉個例子,如果我們要查找2元素,那么先與6比較,比6小,那么進入6的左子樹,再與5進行比較,比5小,進入5的左子樹,我們就成功地找到了2。這比我們暴力查找要快得多。但是缺點也很顯著。比如說,如果輸入數據滿足單調遞增性質,那么我們在建樹時便會將其建成一條鏈,從而導致其算法的復雜度退化。當然,特定情況下,我們可以借助random—shuffle函數將數據打亂,但是一般情況下,BST便有着很大的局限性。因此我們需要一種更高級的數據結構來克服這種局限性,便是我們的Treap。
Treap=Tree+heap,顧名思義,Treap便是一種樹與堆的結合體。總的來說,就是在維護BST性質的同時,給與每個節點一個隨機的random值,同時保證random值滿足小根堆的性質。這樣,便可以輕而易舉的防止復雜度的退化。
在了解了Treap的原理之后,我們便可以嘗試用代碼來實現其功能。下面以洛谷-P3369普通平衡樹為例,一道典型並且操作齊全的模版題,其主要要求我們完成6個操作:
-
插入x數
-
刪除x數(若有多個相同的數,因只刪除一個)
-
查詢x數的排名(排名定義為比當前數小的數的個數+1。若有多個相同的數,因輸出最小的排名)
-
查詢排名為x的數
-
求x的前驅(前驅定義為小於x,且最大的數)
-
求x的后繼(后繼定義為大於x,且最小的數)
首先便是隨機值的實現。由於<stdlib>中的rand()函數速度較慢且局限性較大,在數據結構中不太適用,所以在這里建議rand函數的功能用手寫來實現。
隨機函數:
int rand()
{ int seed=12345; return seed=(int)seed*482711LL%2147483647; }
稍微優化后可以變為這樣(雖然沒什么用):
inline int rand()
{ static int seed=12345; return seed=(int)seed*482711LL%2147483647; }
然后還需要一個函數來更新每個節點的信息,也是十分的淺顯易懂:
void update(int p) { size[p]=size[l[p]]+size[r[p]]+ct[p]; }
在我們維護Treap的過程中,子樹大小的維護也時時刻刻都是有必要的,在每個函數中都應該有體現,具體維護方式如下:
對於旋轉,我們要在旋轉后對子節點和根節點分別重新計算其子樹的大小。
對於插入,在尋找插入的位置時,每經過一個節點,都要先使以它為根的子樹的大小增加 1,再遞歸進入子樹查找。
對於刪除,在尋找待刪除節點,遞歸返回時要把所有的經過的節點的子樹的大小減少 1。要注意的是,刪除之前一定要保證待刪除節點存在於 Treap 中。
維護子樹的大小也是Treap的一個關鍵部分。
那么現在便來到了最關鍵也是Treap中最核心的一步:如何維護堆的性質,即如何在Treap中插入元素(ins)。
對於Treap中的每個元素,為保證我們堆的性質,插入操作便分為了兩種操作:左旋(lturn),右旋(rturn)。下面重點講解這兩種操作。
下面畫了一個圖以便理解:
以上圖為例,我們可以看到,從左到右便是右旋的過程,使得根節點由u變為了x。由於a仍比x小,所以x的左子樹仍為a,u比x大,所以為x的右子樹,但對於b,大於x小於u,所以應在x的右子樹,u的左子樹,同理,c應在u的右子樹,旋轉完畢。這便是右旋的過程。
理解了右旋的過程之后,我們也可以較為輕松的寫出右旋的代碼,為了方便,加了個小小的傳引用:
void rturn(int &k) { int t=l[k];//記錄左兒子 l[k]=r[t]; r[t]=k;//旋轉的過程 size[t]=size[k];//size的轉換 update(k);//更新k k=t; }
左旋轉的過程就是上圖從右到作的過程,代碼實現也同理:
void lturn(int &k) { int t=r[k];//記錄右兒子 r[k]=l[t]; l[t]=k;//旋轉 size[t]=size[k];///size轉換 update(k);//更新k k=t; }
了解這兩種操作后,插入元素就變得得心應手了,先把要插入的點插入到一個葉子上,然后跟維護堆一樣,如果當前節點的優先級比根大就旋轉,如果當前節點是根的左兒子就右旋,如果當前節點是根的右兒子就左旋。
依然舉兩個例子來幫助理解:
如圖所示,我們需要把D和F元素插入到Treap中,對於D,先將其放在一個葉子節點,然后與其父親相比較發現比父親小卻在父親的右子樹上,所以我們需要對D進行右旋操作,同理,F元素經過一次次的比較,一次次的旋轉,最終也可以到達如圖所示的位置。
至此,我們已經基本解決了對於Treap的插入操作。
代碼如下:
void ins(int &p,int x) { if (p==0) { p=++sz; size[p]=ct[p]=1,v[p]=x,rnd[p]=rand(); return; } size[p]++; if (v[p]==x) ct[p]++; else if (x>v[p]) { ins(r[p],x); if (rnd[r[p]]<rnd[p]) lturn(p); } else { ins(l[p],x); if (rnd[l[p]]<rnd[p]) rturn(p); } }
接下來是刪除操作(del),刪除操作算是Treap中最難理解的操作了吧(主要因為代碼長╮(╯▽╰)╭)。本可以通過兩種方式來達成刪除操作,但對於初學者來講,這里推薦並主要講解其中一種方式。
注意到Treap的性質,即必須滿足堆的性質,所以對於Treap,我們也可以用刪除堆的方式,借助旋轉操作,加以解決。
如果該節點的左子節點的key小於右子節點的key,右旋該節點,使該節點降為右子樹的根節點,然后訪問右子樹的根節點,遞歸地操作下去;反之同理。實質上即為讓key小的節點有限旋到上面,保證堆的性質,進而進行刪除操作。
刪除操作比較難以理解,希望通過代碼可以加深對其的認識。
代碼實現:
void del(int &p,int x) { if (p==0) return; if (v[p]==x) { if (ct[p]>1) ct[p]--,size[p]--; else { if (l[p]==0||r[p]==0) p=l[p]+r[p]; else if (rnd[l[p]]<rnd[r[p]]) rturn(p),del(p,x); else lturn(p),del(p,x); } } else if (x>v[p]) size[p]--,del(r[p],x); else size[p]--,del(l[p],x); }
解決完刪除操作后,查找(query)操作便顯得較為簡單,按照一般樹上問題解決方式統計即可,這里不多贅述,其中query1代表查詢x數的排名,query2代表查詢排名為x的數。
代碼實現:
int query1(int p,int x) { if (p==0) return 0; if (v[p]==x) return size[l[p]]+1; if (x>v[p]) return size[l[p]]+ct[p]+query1(r[p],x); else return query1(l[p],x); }
int query2(int p,int x) { if (p==0) return 0; if (x<=size[l[p]]) return query2(l[p],x); x-=size[l[p]]; if (x<=ct[p]) return v[p]; x-=ct[p]; return query2(r[p],x); }
最后,我們來處理一下前驅與后繼的問題。前驅定義為小於x,且最大的數,后繼定義為大於x,且最小的數,也較為簡單,過程中維護一下max和min即可輕易地解決。
該部分代碼:
int findfront(int p,int x) { if (p==0) return -inf; if (v[p]<x) return max(v[p],findfront(r[p],x)); else if (v[p]>=x) return findfront(l[p],x); }
int findback(int p,int x) { if (p==0) return inf; if (v[p]<=x) return findback(r[p],x); else return min(v[p],findback(l[p],x)); }
至此,Treap中的所有操作都已經解決。將這些操作拼接串聯起來,便構成了Treap的基本框架,完整模版如下(以普通平衡樹為例):
#include <stdio.h> #include <algorithm> #include <stdlib.h> using namespace std; #define inf 300000030 int l[100100],r[100100],v[100100],size[100100],rnd[100100],ct[100100]; int sz; void update(int p) { size[p]=size[l[p]]+size[r[p]]+ct[p]; } void lturn(int &k) { int t=r[k]; r[k]=l[t]; l[t]=k; size[t]=size[k]; update(k); k=t; } void rturn(int &k) { int t=l[k]; l[k]=r[t]; r[t]=k; size[t]=size[k]; update(k); k=t; } void ins(int &p,int x) { if (p==0) { p=++sz; size[p]=ct[p]=1,v[p]=x,rnd[p]=rand(); return; } size[p]++; if (v[p]==x) ct[p]++; else if (x>v[p]) { ins(r[p],x); if (rnd[r[p]]<rnd[p]) lturn(p); } else { ins(l[p],x); if (rnd[l[p]]<rnd[p]) rturn(p); } } void del(int &p,int x) { if (p==0) return; if (v[p]==x) { if (ct[p]>1) ct[p]--,size[p]--; else { if (l[p]==0||r[p]==0) p=l[p]+r[p]; else if (rnd[l[p]]<rnd[r[p]]) rturn(p),del(p,x); else lturn(p),del(p,x); } } else if (x>v[p]) size[p]--,del(r[p],x); else size[p]--,del(l[p],x); } int query1(int p,int x) { if (p==0) return 0; if (v[p]==x) return size[l[p]]+1; if (x>v[p]) return size[l[p]]+ct[p]+query1(r[p],x); else return query1(l[p],x); } int query2(int p,int x) { if (p==0) return 0; if (x<=size[l[p]]) return query2(l[p],x); x-=size[l[p]]; if (x<=ct[p]) return v[p]; x-=ct[p]; return query2(r[p],x); } int findfront(int p,int x) { if (p==0) return -inf; if (v[p]<x) return max(v[p],findfront(r[p],x)); else if (v[p]>=x) return findfront(l[p],x); } int findback(int p,int x) { if (p==0) return inf; if (v[p]<=x) return findback(r[p],x); else return min(v[p],findback(l[p],x)); } int ss; int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++) { int flag,x; scanf("%d%d",&flag,&x); if (flag==1) ins(ss,x); if (flag==2) del(ss,x); if (flag==3) printf("%d\n",query1(ss,x)); if (flag==4) printf("%d\n",query2(ss,x)); if (flag==5) printf("%d\n",findfront(ss,x)); if (flag==6) printf("%d\n",findback(ss,x)); } }