【瞎口胡/史前巨坑】Treap 學習筆記
友情提示:這篇博文是寫給自己複習的,可能寫的比較爛,不建議初學者學習。
二叉搜尋樹(二叉排序樹,BST)是一種特殊的二叉樹。這種二叉樹帶點權(\(\text{key}\)),且它滿足對於任意節點,其左子樹中的節點的權值均小於它,右子樹中的節點的權值均大於它。
容易發現,對任意一棵 BST 進行中序遍歷,得到的序列遞增。
對上圖中的 BST 進行中序遍歷,得到 \([1,4,8,9,12,13]\)。
可以看看這道題。裡面的操作 BST 都能做,也有很多部落格講過了。
容易發現,BST 的期望樹高是 \(O(\log n)\) 的,然而,對於一個序列,其對應的 BST 不一定唯一。
這棵 BST 的中序遍歷和第一棵 BST 一樣,但是樹高增加了很多。對於這種 BST,樹高變成了 \(O(n)\)
所以我們需要一些措施來防止 BST 退化。最好寫的就是 Treap 啦。
Treap 中,每個 BST 節點除了點權 \(\text{key}\) 還有隨機權值 \(\text{rnd}\)。任意一棵 Treap 需要滿足:
- 以節點的 \(\text{key}\) 為點權構成的二叉樹符合 BST 的全部性質
- 以節點的 \(\text{rnd}\) 為點權構成的二叉樹符合堆的全部性質
這棵樹就是 Treap。但是因為 \(\text{rnd}\) 是隨機的,最壞情況下仍然會退化。但是出題人總不會對著隨機種子卡你啊,所以樹高基本上是 \(O(\log n)\)
推薦幾個好用的隨機種子:
- \(114514\),\(1919810\),\(998244353\),\(20071005\)
- 當然,
srand(time(NULL))
才是王道
那麼這麼優秀的平衡樹要怎麼寫呢?
有兩種 Treap,一種 Treap 帶旋轉,一種是非旋。帶旋轉的 Treap 難寫難調,容易轉錯;非旋 Treap 好寫,不容易出錯。
非旋 Treap,又名 FHQ-Treap。核心操作是兩個:split 和 merge。請忘掉上面所有的 BST 操作,基本用不著。
split
split 操作將一個 Treap 拆成 \(x,y\) 兩棵,\(x\) 中所有 \(\text{key} \leq v\)
void split(int now,int v,int &x,int &y){
if(!now){ // 分完了
x=y=0;
return;
}
update(now); // 提前更新子樹資訊
if(tree[now].val<=v){ // now 和 now 的左子樹分到 x
x=now;
split(rc(now),v,rc(x),y); // 將 now 的右子樹繼續拆分
update(x);
}else{ // now 和 now 的右子樹分到 y
y=now;
split(lc(now),v,x,lc(y));
update(y);
}
return;
}
還有一種 split 以子樹大小劃分,中序遍歷前 \(k\) 個分到 \(x\),剩餘部分分到 \(y\)。這種 split 在維護序列的時候非常有用(可以提取一個序列中的某個區間 \([l,r]\))。
void split(int now,int k,int &x,int &y){
if(!now){
x=y=0;
return;
}
update(now),pushdown(now); // 一部分序列維護問題需要區間打標記,在 split 前需要把所有標記下傳
if(tree[lc(now)].size<k){
x=now;
split(rc(now),k-tree[lc(now)].size-1,rc(x),y);
update(x);
}else{
y=now;
split(lc(now),k,x,lc(y));
update(y);
}
return;
}
merge
merge 將兩棵平衡樹 \(x,y\) 合併成一棵。當 \(\max\limits_{a \in x} \{\text{key}_a \} \leq \max\limits_{b \in y} \{\text{key}_b \}\)(\(x\) 裡面的任意 \(\text{key}\) 不比 \(y\) 中的大)的時候,這才是對的。
特殊地,在維護序列的時候,merge 操作的意義是將兩個連續的區間合併成一個,但寫法並沒有改變。
void merge(int &now,int x,int y){
if(!x||!y){
now=x|y;
return;
}
update(x),update(y);
if(tree[x].rnd<tree[y].rnd){ // 維護堆性質(大根堆小根堆都行啦)
now=x;
merge(rc(now),rc(x),y);
update(now);
}else{
now=y;
merge(lc(now),x,lc(y));
update(now);
}
return;
}
現在再來看看 這道題。
- 插入 \(x\)
inline void Insert(int v){
int x,a,b;
NewNode(x,v); // 新建節點
split(root,v,a,b);
merge(a,a,x);
merge(root,a,b); // 記得合併回去
return;
}
- 刪除 \(x\)
因為有多個 \(x\) 時只刪除一個,所以我們沒辦法直接將 \(\text{key}\) 等於 \(x\) 的子樹提出來扔掉。將子樹提出來之後,應該合併子樹根的左右兒子成為一棵新子樹,相當於消除了根節點。最後合併回去,就好啦。
inline void Delete(int v){
int a,b,c;
split(root,v,a,c);
split(a,v-1,a,b);
merge(b,lc(b),rc(b));
merge(a,a,b);
merge(root,a,c);
return;
}
- 查詢 \(x\) 的排名
將小於 \(x\) 的子樹提出來,再加上 \(1\) 就是答案。
inline int Rank(int x){
int a,b;
split(root,x-1,a,b);
int ans=tree[a].size+1;
merge(root,a,b);
return ans;
}
- 求第 \(x\) 大的數
BST 基本操作。
inline int Kth(int x,int k){
assert(tree[x].size>=k); // 如果以 x 為根的子樹大小 <= k,那一定傳引數的時候寫假了,丟擲 RE 來檢查
while(1){
if(k<=tree[lc(x)].size){
x=lc(x);
}else if(k==tree[lc(x)].size+1){
return tree[x].val;
}else{
k-=tree[lc(x)].size+1;
x=rc(x);
}
}
return 114514; // 當然,這裡是永遠也跑不到的,但是控制流達到非 void 函式末尾會報錯...
}
- 求 \(x\) 的前驅
將小於 \(x\) 的子樹提出來,子樹最大值就是答案。子樹最大值可以 kth 求。
inline int GetPre(int x){
int a,b;
split(root,x-1,a,b);
int ans=Kth(a,tree[a].size);
merge(root,a,b);
return ans;
}
- 求 \(x\) 的後繼。
同上。
完整程式碼:
# include <bits/stdc++.h>
# define rr register
const int N=100010;
struct Node{
int val,son[2],rnd,size;
}tree[N];
int root,cnt;
inline int &lc(int x){
return tree[x].son[0];
}
inline int &rc(int x){
return tree[x].son[1];
}
inline void NewNode(int &x,int v){
x=++cnt;
tree[x].rnd=rand(),tree[x].size=1,tree[x].val=v;
return;
}
inline void update(int x){
tree[x].size=tree[lc(x)].size+tree[rc(x)].size+1;
return;
}
void split(int now,int v,int &x,int &y){
if(!now){
x=y=0;
return;
}
update(now);
if(tree[now].val<=v){
x=now;
split(rc(now),v,rc(x),y);
update(x);
}else{
y=now;
split(lc(now),v,x,lc(y));
update(y);
}
return;
}
void merge(int &now,int x,int y){
if(!x||!y){
now=x|y;
return;
}
update(x),update(y);
if(tree[x].rnd<tree[y].rnd){
now=x;
merge(rc(now),rc(x),y);
update(now);
}else{
now=y;
merge(lc(now),x,lc(y));
update(now);
}
return;
}
inline int Kth(int x,int k){
assert(tree[x].size>=k);
while(1){
if(k<=tree[lc(x)].size){
x=lc(x);
}else if(k==tree[lc(x)].size+1){
return tree[x].val;
}else{
k-=tree[lc(x)].size+1;
x=rc(x);
}
}
return 114514;
}
inline void Insert(int v){
int x,a,b;
NewNode(x,v);
split(root,v,a,b);
merge(a,a,x);
merge(root,a,b);
return;
}
inline void Delete(int v){
int a,b,c;
split(root,v,a,c);
split(a,v-1,a,b);
merge(b,lc(b),rc(b));
merge(a,a,b);
merge(root,a,c);
return;
}
inline int Rank(int x){
int a,b;
split(root,x-1,a,b);
int ans=tree[a].size+1;
merge(root,a,b);
return ans;
}
inline int GetPre(int x){
int a,b;
split(root,x-1,a,b);
int ans=Kth(a,tree[a].size);
merge(root,a,b);
return ans;
}
inline int GetNext(int x){
int a,b;
split(root,x,a,b);
int ans=Kth(b,1);
merge(root,a,b);
return ans;
}
inline int read(void){
int res,f=1;
char c;
while((c=getchar())<'0'||c>'9')
if(c=='-')f=-1;
res=c-48;
while((c=getchar())>='0'&&c<='9')
res=res*10+c-48;
return res*f;
}
void print(int x){
if(!x)
return;
putchar('['),print(lc(x)),printf("%d(%d)",tree[x].val,tree[x].size),print(rc(x)),putchar(']');
return;
}
int main(void){
int n=read();
while(n--){
int opt=read();
switch(opt){
case 1:{
Insert(read());
break;
}
case 2:{
Delete(read());
break;
}
case 3:{
printf("%d\n",Rank(read()));
break;
}
case 4:{
printf("%d\n",Kth(root,read()));
break;
}
case 5:{
printf("%d\n",GetPre(read()));
break;
}
case 6:{
printf("%d\n",GetNext(read()));
break;
}
default:{
break;
}
}
}
return 0;
}