1. 程式人生 > 實用技巧 >【瞎口胡/史前巨坑】Treap 學習筆記

【瞎口胡/史前巨坑】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\)

,而 \(y\) 中所有 \(\text{key} > 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;
}