平衡樹入門——FHQ_Treap
平衡樹入門——FHQ_Treap
1 簡介
FHQ treap,也有人稱之為無旋 Treap,因為沒有旋轉,所以可以支援可持久化,也可以支援序列操作,常數略大,速度比 Splay 快。用兩個操作——插入和刪除就完成了對整個 Treap 的維護。自我感覺程式碼複雜度比 Treap,Splay 很低。容易理解,理解後記憶程式碼十分容易。
2 資料結構解析
2.1 節點資訊
FHQ Treap 和 Treap 的節點資訊是一樣的——除了一點以外:
struct node{
int val,size,key,l,r;
};
node p[N];
相信讀者已經發現了,FHQ Treap 沒有記錄相同權值節點的個數,也就是說,就算有相同權值的節點,也不能夠把這些節點當做一個節點來處理。這帶來了一定意義上空間的浪費,不過少維護一些東西可以讓程式設計複雜度降低。
這裡 \(val\) 是權值,\(size\) 是子樹大小,\(key\) 是 Treap 的隨機值,\(l,r\) 分別是左右兒子。
2.2 創造新節點
inline int new_node(int val){
p[++tot].val=val;p[tot].key=random(INF);
p[tot].l=p[tot].r=0;p[tot].size=1;
return tot;
}
其中:
inline int random(int n){
return rand()*rand()%n+1;
}
這裡 \(tot\)
2.3 合併資訊
inline void pushup(int k){
p[k].size=p[p[k].l].size+p[p[k].r].size+1;
}
太過於簡單,不作講解。
2.4 分裂
inline void split(int k,int val,int &x,int &y){//depend on val if(k==0){ x=y=0;return; } if(p[k].val<=val){ x=k; split(p[k].r,val,p[k].r,y); } else{ y=k; split(p[k].l,val,x,p[k].l); } pushup(k); }
使用 split(k,val_,x,y)
就相當於完成了這樣一件事情:把以 \(k\) 為根的子樹按照權值分裂,其中權值小於等於 val_
的最終會到以 \(x\) 為根的樹中,大於 val_
的權值會到以 \(y\) 為根的樹中。注意到 \(x\) 和 \(y\) 是通過引用傳回來的。
我們現在重點關注一下他是如何分裂的,首先,如果 \(k=0\) 那麼這就代表已經分裂結束或者這顆樹為空,不管是哪一種情況,這個時候,讓引用的 \(x,y\) 等於 \(0\) 是不影響結果的,因為這個時候並沒有那一顆子樹被分到了以 \(x\) 為根或以 \(y\) 為根的子樹上去。
否則,如果這個節點的權值是小於等於 \(val\) 的,說明 \(k\) 這個節點和 \(k\) 這個節點的左子樹都會被劃分到 \(x\) 這顆子樹上去,而此時此刻,\(k\) 這個節點的右子樹還沒有被劃分,所以我們在去劃分一下 \(k\) 的右子樹,注意我們是帶引用的去進行遞迴的,所以如果有要劃分到 \(x\) 這個子樹上的節點,我們就把它掛到右子樹上去,這也是為什麼我們把第三個引數變為 p[k].r
。第 \(9\) 到第 \(12\) 行同理。
這裡的引用有效降低了程式設計複雜度。
2.5 合併
inline int merge(int x,int y){
if(x==0||y==0) return x+y;
if(p[x].key>p[y].key){
p[x].r=merge(p[x].r,y);
pushup(x);
return x;
}
else{
p[y].l=merge(x,p[y].l);
pushup(y);
return y;
}
}
這一段程式碼完成的是把以 \(x\) 為根的子樹與以 \(y\) 為根的子樹合併,注意這裡保證以 \(x\) 為根的子樹的權值最大值要小於以 \(y\) 為根的子樹的權值最小值。注意這裡我們需要維護優先順序。因為有上面那個性質,所以我們不用判斷節點權值大小而可以直接合並,最後這段程式碼傳回的是合併完兩顆子樹後的根節點。
不難理解這段程式碼:如果 \(x\) 的優先值大於 \(y\) 的優先值,如圖:
不難發現的是,\(x\) 的左子樹就不需要進行合併的,需要在進行合併的是 \(x\) 的右子樹和 \(y\) 這顆子樹,遞迴進行就可以。\(y\) 的優先順序更大是同理的。如果其中一顆子樹為空,那麼我們直接返回 \(x+y\) 就可以,如果另一顆子樹不為空那麼返回的就是另一顆子樹的根節點,如果兩顆子樹都為空也不失正確性。
2.6 插入
其實有了上面這兩個操作其它操作的實現就非常簡單了。
inline void insert(int val){
int x,y;
split(root,val-1,x,y);
root=merge(merge(x,new_node(val)),y);
}
這個函式能夠往平衡樹中插入一個權值為 \(val\) 的節點。
如何實現的呢?我們按照權值 \(val-1\) 來進行分裂,分裂之後,權值小於等於 \(val-1\) 的節點都在以 \(x\) 為根的子樹中,其他節點在以 \(y\) 為根的子樹中,然後我們先把 \(x\) 與我們新建的節點合併,然後再合併整棵樹。
2.7 刪除
inline void delete_(int val){
int x,y,z;
split(root,val,x,z);
split(x,val-1,x,y);
if(y){
y=merge(p[y].l,p[y].r);
}
root=merge(merge(x,y),z);
}
不難發現在分裂之後,以 \(y\) 為根的子樹裡只有權值等於 \(val\) 的節點,我們合併左右子樹——刪除根就可以。
如果要刪除所有權值為 \(val\) 的節點,就不用寫第 \(6\) 到第 \(9\) 行。
刪完之後,我們把整棵樹重新合併。
2.8 查詢排名
inline int getrank(int val){
int x,y,ans;
split(root,val-1,x,y);
ans=p[x].size+1;
root=merge(x,y);
return ans;
}
某個數的排名就是比他小的數的個數 \(+1\) ,所以不難理解上面這個程式碼。
2.9 查詢值
inline int getval(int rank){
int k=root;
while(k){
if(p[p[k].l].size+1==rank) break;
else if(p[p[k].l].size>=rank) k=p[k].l;
else rank-=p[p[k].l].size+1,k=p[k].r;
}
return k==0?INF:p[k].val;
}
這個和普通的平衡樹查詢值是一樣的,不作講解。
2.10 查詢前驅後繼
inline int getpre(int val){
int x,y,k,ans;
split(root,val-1,x,y);
k=x;
while(p[k].r) k=p[k].r;
ans=p[k].val;
root=merge(x,y);
return ans;
}
inline int getnext(int val){
int x,y,k,ans;
split(root,val,x,y);
k=y;while(p[k].l) k=p[k].l;
ans=p[k].val;
root=merge(x,y);
return ans;
}
如果要查詢前驅,我們就按照 \(val-1\) 分裂整顆樹,然後取 \(x\) 子樹最靠右的節點就可以了,查詢後繼同理。
2.11 優化
因為在一定程度上 FHQ Treap 有點浪費空間,所以我們可以開一個棧,把所以刪除的節點編號存一下,然後插入新節點時我們優先使用這些被刪除的節點。程式碼就看這篇部落格,同時這篇部落格還講解了如何判斷某一個節點是否存在,以及返回整顆輸的大小等操作,相信有了分裂和合並,這些操作不難實現。
引用
當你想要結束的時候,想想你為什麼開始!