「演算法筆記」Treap
一、引入
隨機資料中,BST 一次操作的期望複雜度為 \(\mathcal{O}(\log n)\)。
然而,BST 很容易退化,例如在 BST 中一次插入一個有序序列,將會得到一條鏈,平均每次操作的複雜度為 \(\mathcal{O}(n)\)。我們稱這種左右子樹大小相差很大的 BST 是“不平衡”的。
有很多方法可以維持 BST 的平衡,從而產生了各種平衡樹。
Treap 就是常見平衡樹中的一種。
二、簡介
滿足 BST 性質且中序遍歷為相同序列的二叉查詢樹是不唯一的。這些二叉查詢樹是等價的,它們維護的是相同的一組數值。在這些二叉查詢樹上執行同樣的操作,將得到相同的結果。
因此,我們可以在維持 BST 性質的基礎上,通過改變二叉查詢樹的形態
Treap 改變形態並保持 BST 性質的方式為“旋轉”,並且保持平衡而不至於退化為鏈。
Treap=Tree+Heap。Treap 是利用堆的性質來維護平衡的一種平衡樹。對每個節點額外儲存一個隨機值,根據隨機值調整 Treap 的形態,使其滿足 BST 性質外,還滿足父節點的隨機值\(\geq\)子節點的隨機值。
三、Treap
前面說過,為了使 Treap 保持平衡而進行旋轉操作。
旋轉的本質是將某個節點上移一個位置。旋轉需要保證 :
-
整棵樹的中序遍歷不變(不能破壞 BST 的性質)。
-
受影響的節點維護的資訊依然正確有效。
每個節點在建立時,賦予其一個隨機值,通過旋轉操作使得隨機值滿足大根堆的性質。這樣可以使得樹高期望保持在 \(\mathcal{O}(\log n)\)。
1. 旋轉操作
在 Treap 中的旋轉分為兩種:左旋和右旋。如下圖所示。
以右旋為例。在初始情況下,\(x\) 是 \(y\) 的左子節點,\(A\) 和 \(B\) 分別是 \(x\) 的左右子樹,\(C\) 是 \(y\) 的右子樹。
“右旋”操作在保持 BST 性質的基礎上,把 \(x\) 變為 \(y\) 的父節點。因為 \(x\) 的關鍵碼小於 \(y\) 的關鍵碼,所以 \(y\)
當 \(x\) 變成 \(y\) 的父節點後,\(y\) 的左子樹就空了出來,於是 \(x\) 原來的右子樹 \(B\) 就恰好作為 \(y\) 的左子樹。
-
左旋:將右兒子提到當前節點,自己作為右兒子的左兒子,右兒子原來的左兒子變成自己新的右兒子。
-
右旋:將左兒子提到當前節點,自己作為左兒子的右兒子,左兒子原來的右兒子變成自己新的左兒子。
旋轉後的二叉樹仍滿足二叉搜尋樹的條件。
void zig(int &p){ //右旋操作。zig(p) 可以理解成把 p 的左子節點繞著 p 向右旋轉。 int q=lc[p]; lc[p]=rc[q],rc[q]=p,p=q; //注意 p 是引用 } void zag(int &p){ //左旋操作。zag(p) 可以理解成把 p 的右子節點繞著 p 向左旋轉。 int q=rc[p]; rc[p]=lc[q],lc[q]=p,p=q; //注意 p 是引用 }
2. 隨機權值
合理的旋轉操作可使 BST 更“平衡”。如下圖,經過一些旋轉操作,這棵 BST 變得比較平衡了。
在隨機資料下,普通的 BST 就是趨近平衡的。Treap 的思想就是利用“隨機”來創造平衡條件。因為在旋轉過程中必須維持 BST 性質,所以 Treap 就把“隨機”作用在堆性質上。
具體來說,Treap 在插入每個新節點時,給該節點隨機生成一個額外的權值。當某個節點不滿足大根堆性質時,就執行旋轉操作,使該點與其父節點的關係發生對換。
每次刪除/插入時通過隨機的值決定要不要旋轉即可,其他操作與 BST 類似。
特別地,對於刪除操作,由於 Treap 支援旋轉,我們可以直接找到需要刪除的節點,並把它向下旋轉成葉節點,最後直接刪除。這樣就避免了採取類似普通 BST 的刪除方法可能導致的節點資訊更新、堆性質維護等複雜問題。
Treap 通過適當的旋轉操作,在維持節點關鍵碼滿足 BST 性質的同時,還使每個節點上隨機生成的額外權值滿足大根堆性質。Treap 是一種平衡的二叉查詢樹,檢索、插入、求前驅後繼以及刪除節點的時間複雜度都是\(\mathcal{O}(\log n)\)。
四、模板
Luogu P3369 普通平衡樹
題目大意:你需要寫一種資料結構,來維護一些數,其中需要提供以下操作:
- 插入數值 \(x\)
- 刪除數值 \(x\)(若有多個相同的數,應只刪除一個)
- 查詢數值 \(x\) 的排名(若有多個相同的數,應輸出最小的排名)
- 查詢排名為 \(x\) 的數
- 求數值 \(x\) 的前驅(前驅定義為小於 \(x\) 的最大的數)
- 求數值 \(x\) 的後繼(後繼定義為大於 \(x\) 的最小的數)
\(1\leq n \leq 10^5,|x| \leq 10^7\)。
Solution:平衡樹模板題,用 Treap 實現即可。
資料中可能有相同的數值。記 \(cnt(u)\) 表示節點 \(u\) 對應數值的出現次數,初始時為 \(1\)。(這裡的“對應數值”就是關鍵碼)
若插入已經存在的數值,就直接把 \(cnt\) 值加 \(1\)。刪除時,若 \(cnt(u)>1\),則把 \(cnt(u)\) 減 \(1\);否則刪除該節點。
再記 \(sz(u)\) 表示以 \(u\) 為根的子樹中所有節點的 \(cnt\) 之和。在插入或刪除時從下往上更新 \(sz\) 資訊。另外,在旋轉操作時,也需要同時修改 \(sz\)。
在 BST 檢索的基礎上,通過判斷 \(sz(lc(u))\) 和 \(sz(rc(u))\) 的大小,選擇適當的一側遞迴,就能查詢排名了。
在插入和刪除操作時,Treap 的形態會發生變化,一般使用遞迴實現,以便於在回溯時更新 Treap 上儲存的 \(sz\) 等資訊。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e5+5; int n,opt,x,tot,rt,lc[N],rc[N],val[N],rnd[N],sz[N],cnt[N],ans; //rnd(u) 表示節點 u 的隨機值 void upd(int p){ sz[p]=sz[lc[p]]+sz[rc[p]]+cnt[p]; } int getnew(int k){ val[++tot]=k,rnd[tot]=rand(),cnt[tot]=sz[tot]=1; return tot; } void build(){ getnew(-1e18),getnew(1e18),rt=1,rc[1]=2,upd(rt); } void rotate(int &p,int dir){ //dir= 0 右旋 1 左旋 int q=!dir?lc[p]:rc[p]; if(!dir) lc[p]=rc[q],rc[q]=p,p=q,upd(rc[p]),upd(p); else rc[p]=lc[q],lc[q]=p,p=q,upd(lc[p]),upd(p); } void insert(int &p,int k){ if(!p){p=getnew(k);return ;} if(val[p]==k){cnt[p]++,upd(p);return ;} if(k<val[p]){insert(lc[p],k);if(rnd[p]<rnd[lc[p]]) rotate(p,0);} //不滿足堆性質,右旋 else{insert(rc[p],k);if(rnd[p]<rnd[rc[p]]) rotate(p,1);} //不滿足堆性質,左旋 upd(p); } void del(int &p,int k){ if(!p) return ; if(val[p]==k){ //檢索到 k if(cnt[p]>1){cnt[p]--,upd(p);return ;} //有重複,讓 cnt 值減 1 即可 if(lc[p]||rc[p]){ //不是葉子節點,向下旋轉 if(!rc[p]||rnd[lc[p]]>rnd[rc[p]]) rotate(p,0),del(rc[p],k); else rotate(p,1),del(lc[p],k); upd(p); } else p=0; return ; //葉子節點直接刪除 } del(k<val[p]?lc[p]:rc[p],k),upd(p); } int rank(int p,int k){ if(!p) return 0; if(val[p]==k) return sz[lc[p]]+1; return k<val[p]?rank(lc[p],k):rank(rc[p],k)+sz[lc[p]]+cnt[p]; } int Kth(int p,int rk){ if(!p) return 1e18; if(sz[lc[p]]>=rk) return Kth(lc[p],rk); if(sz[lc[p]]+cnt[p]>=rk) return val[p]; return Kth(rc[p],rk-sz[lc[p]]-cnt[p]); } int pre(int k){ int ans=1,p=rt; while(p){ if(val[p]==k){ if(lc[p]>0){p=lc[p]; while(rc[p]>0) p=rc[p]; ans=p;} //左子樹上一直向右走 break; } if(val[p]<k&&val[p]>val[ans]) ans=p; p=k<val[p]?lc[p]:rc[p]; } return val[ans]; } int nxt(int k){ int ans=2,p=rt; while(p){ if(val[p]==k){ if(rc[p]>0){p=rc[p]; while(lc[p]>0) p=lc[p]; ans=p;} //右子樹上一直向左走 break; } if(val[p]>k&&val[p]<val[ans]) ans=p; p=k<val[p]?lc[p]:rc[p]; } return val[ans]; } signed main(){ scanf("%lld",&n),build(); while(n--){ scanf("%lld%lld",&opt,&x),ans=-1; if(opt==1) insert(rt,x); else if(opt==2) del(rt,x); else if(opt==3) ans=rank(rt,x)-1; else if(opt==4) ans=Kth(rt,x+1); else if(opt==5) ans=pre(x); else ans=nxt(x); if(~ans) printf("%lld\n",ans); } return 0; }
少了一點壓行的版本:
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e5+5; int n,opt,x,tot,rt,lc[N],rc[N],val[N],rnd[N],sz[N],cnt[N],ans; //rnd(u) 表示節點 u 的隨機值 void upd(int p){ sz[p]=sz[lc[p]]+sz[rc[p]]+cnt[p]; } int getnew(int k){ val[++tot]=k,rnd[tot]=rand(),cnt[tot]=sz[tot]=1; return tot; } void build(){ getnew(-1e18),getnew(1e18),rt=1,rc[1]=2,upd(rt); } void zig(int &p){ //右旋 int q=lc[p]; lc[p]=rc[q],rc[q]=p,p=q,upd(rc[p]),upd(p); } void zag(int &p){ //左旋 int q=rc[p]; rc[p]=lc[q],lc[q]=p,p=q,upd(lc[p]),upd(p); } void insert(int &p,int k){ if(!p){p=getnew(k);return ;} if(val[p]==k){cnt[p]++,upd(p);return ;} if(k<val[p]){ insert(lc[p],k); if(rnd[p]<rnd[lc[p]]) zig(p); //不滿足堆性質,右旋 } else{ insert(rc[p],k); if(rnd[p]<rnd[rc[p]]) zag(p); //不滿足堆性質,左旋 } upd(p); } void del(int &p,int k){ if(!p) return ; if(val[p]==k){ //檢索到 k if(cnt[p]>1){cnt[p]--,upd(p);return ;} //有重複,讓 cnt 值減 1 即可 if(lc[p]||rc[p]){ //不是葉子節點,向下旋轉 if(!rc[p]||rnd[lc[p]]>rnd[rc[p]]) zig(p),del(rc[p],k); else zag(p),del(lc[p],k); upd(p); } else p=0; return ; //葉子節點直接刪除 } del(k<val[p]?lc[p]:rc[p],k),upd(p); } int rank(int p,int k){ if(!p) return 0; if(val[p]==k) return sz[lc[p]]+1; if(k<val[p]) return rank(lc[p],k); return rank(rc[p],k)+sz[lc[p]]+cnt[p]; } int Kth(int p,int rk){ if(!p) return 1e18; if(sz[lc[p]]>=rk) return Kth(lc[p],rk); if(sz[lc[p]]+cnt[p]>=rk) return val[p]; return Kth(rc[p],rk-sz[lc[p]]-cnt[p]); } int pre(int k){ int ans=1,p=rt; while(p){ if(val[p]==k){ if(!(p=lc[p])) break; while(rc[p]>0) p=rc[p]; //左子樹上一直向右走 ans=p;break; } if(val[p]<k&&val[p]>val[ans]) ans=p; p=k<val[p]?lc[p]:rc[p]; } return val[ans]; } int nxt(int k){ int ans=2,p=rt; while(p){ if(val[p]==k){ if(!(p=rc[p])) break; while(lc[p]>0) p=lc[p]; //右子樹上一直向左走 ans=p;break; } if(val[p]>k&&val[p]<val[ans]) ans=p; p=k<val[p]?lc[p]:rc[p]; } return val[ans]; } signed main(){ scanf("%lld",&n),build(); while(n--){ scanf("%lld%lld",&opt,&x),ans=-1; if(opt==1) insert(rt,x); else if(opt==2) del(rt,x); else if(opt==3) ans=rank(rt,x)-1; else if(opt==4) ans=Kth(rt,x+1); else if(opt==5) ans=pre(x); else ans=nxt(x); if(~ans) printf("%lld\n",ans); } return 0; }
五、參考資料
- 《演算法競賽進階指南》(大部分內容摘自這裡 QAQ)