1. 程式人生 > 其它 >淺談 fhq-treap(無旋treap)

淺談 fhq-treap(無旋treap)

淺談 fhq-treap(無旋treap)

模板題:洛谷 P6136 【模板】普通平衡樹(資料加強版)

(emm剛寫了這個,就放這個吧)

\(fhq-treap\) 也叫做 無旋treap,由防火牆範浩強大佬發明。

個人認為是平衡樹中碼量最少,也最容易理解的一種寫法。

主要思想

顧名思義,無旋意味著它沒有旋轉操作(終於沒有噁心人的旋轉了)。

事實上,無旋treap只有兩種操作。

一種是 \(split\)(分裂),另一種是 \(merge\)(合併)。

平衡樹中的各種操作都可以用這兩個函式來搞定。下面我們來一一分析。

常見操作

  • 插入一個整數 \(x\)

  • 刪除一個整數 \(x\)(若有多個相同的數,只刪除一個)。

  • 查詢整數 \(x\) 的排名(排名定義為比當前數小的數的個數 +1)。

  • 查詢排名為 \(x\) 的數(如果不存在,則認為是排名小於 \(x\) 的最大數。保證 \(x\) 不會超過當前資料結構中數的總數)。

  • \(x\) 的前驅(前驅定義為小於 \(x\),且最大的數)。

  • \(x\) 的後繼(後繼定義為大於 \(x\),且最小的數)。

當然區間反轉也是可以用無旋\(treap\) 實現的,這裡先不講。

函式解釋

split(分裂)

\(split\) 函式要實現把一棵平衡樹拆成兩棵,左邊的樹上節點的值都小於等於 \(k\), 右邊的樹上節點的值都大於等於 \(k\)

  • \(x:\) 當前子樹根節點

  • \(k:\)\(k\) 為分界線分割樹

  • \(a, b:\) 分裂之後兩棵子樹的根,左邊的子樹根是 \(a\),右邊的根是 \(b\),這裡要傳地址,方便寫。

注意: 當根為 0 時,一定一定一定要清空 \(a\),&b&,不然會死迴圈(我就在這裡卡了好久QWQ)

inline void split(int x, int k, int &a, int &b){
	if(!x){
		a = b = 0;	//清空清空清空(重要的事情說三遍)
		return;
	}
	if(t[x].val <= k){
		a = x;		//左邊子樹已經分裂出去了,直接a=x
		split(rs(x), k, rs(x), b);	//在右子樹上查詢,把拆下來右子樹上節點值<=k的點放回到x的右子樹上
	}else{
		b = x;		//同理
		split(ls(x), k, a, ls(x));
	}
	pushup(x);
}

merge(合併)

\(merge\) 函式要實現把兩個分開的樹再合併到一起去,並返回合併後的根節點。

  • \(x, y:\) 兩棵樹的根

這個操作類似於線段樹合併。

注意合併時只能是兩個相鄰的子樹合併,不能跳著合併。

例如:樹 \(rt\) ---> 子樹\(a\) 和 子樹\(b\)

子樹\(b\) ---> 子樹\(c\) 和 子樹\(d\) (---> 表示拆成兩棵樹)

那麼我們合併時,只能 \(merge(a, merge(c,d))\)\(merge(merge(a, c), d)\)

而不能 \(merge(merge(a, d), c)\)

inline int  merge(int x, int y){
	if(!x || !y) return x + y;
	if(t[x].wei <= t[y].wei){		//wei是隨機值,wei(x) <= wei(y) 時,讓 y 作為 x 的子樹
		rs(x) = merge(rs(x), y);
		pushup(x);
		return x;
	}else{							//反之,x 作為 y 的子樹
		ls(y) = merge(x, ls(y));
		pushup(y);
		return y;
	}
}

insert(插入)

先新建一個點設為 \(y\),把原樹拆成 \(x\)\(z\)

\(x\) 中節點值 \(<= k\)\(z\) 中節點值 \(>k\)

然後一次合併 \(x\)\(y\)\(z\)即可。

inline void insert(int k){
	t[++tot].val = k, t[tot].wei = rand();
	t[tot].siz = 1;
	split(root, k, a, b);
	root = merge(merge(a, tot), b);
}

remove(刪除)

這個就很巧妙了,我們把值 \(<=k\) 的樹先拆出來設為 \(a\),然後把值 \(<k\) 的點再拆出來,剩餘的部分設為為 \(c\)

那麼現在 \(c\) 中的點就是 \(=k\) 的。

我們直接合並 \(c\) 的左右兩棵子樹,這樣就相當於把根節點刪了。

最後合併回去。

inline void remove(int k){
	split(root, k, a, b);		//拆,<=k的存到a裡,>k的存到b裡。
	split(a, k - 1, a, c);		//再拆,<k的存到a裡,=k的存到裡。
	c = merge(ls(c), rs(c));	//合併c左右子樹
	root = merge(merge(a, c), b);	//再把a,b合併上去。
}

check_rk

查詢 \(k\) 的排名。

我們把 \(<k\) 的的點從樹上分裂出來,然後分出來的樹的大小 +1 就是排名。

別忘了合併回去。

inline int check_rk(int k){
	int rank;
	split(root, k - 1, a, b);
	rank = t[a].siz + 1;
	root = merge(a, b);
	return rank;
}

check_val

查詢排名為 \(k\) 的數。

我這裡用的遞迴寫法。

  • \(x\) 當前子樹的根。

  • \(k\) 在當前子樹中排名多少。

我這裡用的遞迴寫法。(程式碼應該挺好理解的吧)

inline int check_val(int x, int k){
	if(k == t[ls(x)].siz + 1) return t[x].val;
	if(k <= t[ls(x)].siz) return check_val(ls(x), k);	//在左子樹中,直接跑左子樹裡查
	else return check_val(rs(x), k - t[ls(x)].siz - 1);	//在右子樹中,減去左子樹大小,再減1(根),就是在右子樹中的排名。
}

check_pre

查詢前驅。

這個也好說,我們先查排名,設排名為 \(rk\),我們再查排名為 \(rk - 1\) 的數是多少,就是前驅了。

inline int check_pre(int x){
	return check_val(root, check_rk(x) - 1);
}

check_next

查詢 \(x\) 的後繼,基本同理,但略有不同。

因為 \(x\) 可能有多個,我們查 \(x\) 的排名後 +1,不一定是 \(x + 1\),有可能還是 \(x\)

那我們怎麼辦呢?

其實也不難,我們直接查 \(x + 1\) 的排名,沒有?沒關係,就算沒有 \(x + 1\),查出來的也是大於 \(x\) 的最小的數的排名。

然後我們再查一下這個排名的值即可。

inline int check_next(int x){
	return check_val(root, check_rk(x + 1));
}

然後……就沒有然後了吧。

主函式我就不用多說了吧。

完整程式碼

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#define ls(x) t[x].ch[0]
#define rs(x) t[x].ch[1]

using namespace std;

const int N = 2e6;		//原本的數+運算元。最多有1.1e6個數
struct Tree{
	int wei, val, siz, ch[2];
}t[N];
int n, m, tot, root;
int last, ans, a, b, c;

inline int read(){
	int x = 0, f = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
	return x * f;
}

inline void pushup(int x){
	t[x].siz = t[ls(x)].siz + t[rs(x)].siz + 1;
}

inline void split(int x, int k, int &a, int &b){
	if(!x){
		a = b = 0;
		return;
	}
	if(t[x].val <= k){
		a = x;
		split(rs(x), k, rs(x), b);
	}else{
		b = x;
		split(ls(x), k, a, ls(x));
	}
	pushup(x);
}

inline int  merge(int x, int y){
	if(!x || !y) return x + y;
	if(t[x].wei <= t[y].wei){
		rs(x) = merge(rs(x), y);
		pushup(x);
		return x;
	}else{
		ls(y) = merge(x, ls(y));
		pushup(y);
		return y;
	}
}

inline void insert(int k){
	t[++tot].val = k, t[tot].wei = rand();
	t[tot].siz = 1;
	split(root, k, a, b);
	root = merge(merge(a, tot), b);
}

inline void remove(int k){
	split(root, k, a, b);
	split(a, k - 1, a, c);
	c = merge(ls(c), rs(c));
	root = merge(merge(a, c), b);
}

inline int check_rk(int k){
	int rank;
	split(root, k - 1, a, b);
	rank = t[a].siz + 1;
	root = merge(a, b);
	return rank;
}

inline int check_val(int x, int k){
	if(k == t[ls(x)].siz + 1) return t[x].val;
	if(k <= t[ls(x)].siz) return check_val(ls(x), k);
	else return check_val(rs(x), k - t[ls(x)].siz - 1);
}

inline int check_pre(int x){
	return check_val(root, check_rk(x) - 1);
}

inline int check_next(int x){
	return check_val(root, check_rk(x + 1));
}

int main(){
	n = read(), m = read();
	for(int i = 1; i <= n; i++){
		int x;
		x = read();
		insert(x);
	}
	while(m--){
		int op, x;
		op = read(), x = read() ^ last;
		if(op == 1) insert(x);
		if(op == 2) remove(x);
		if(op <= 2) continue;
		if(op == 3) last = check_rk(x);
		if(op == 4) last = check_val(root, x);
		if(op == 5) last = check_pre(x);
		if(op == 6) last = check_next(x);
		ans ^= last;
	}
	printf("%d\n", ans);
	return 0;
}

End

本文來自部落格園,作者:{xixike},轉載請註明原文連結:https://www.cnblogs.com/xixike/p/15132464.html