1. 程式人生 > 其它 >treap【來自蒟蒻的整理】

treap【來自蒟蒻的整理】

(最後更新時間2021/11/21 11:04:49,部落格查詢操作未寫完,見諒)
蒟蒻yzh粉絲有點少,看文章前先點個關注唄,qwq,萌新的日常treap學習,技術欠缺,見諒


前言:

眾所周知平衡樹兩大演算法splay和treap,聽某谷的大佬說treap要快(可能也並非如此),所以我只學了treap
treap:“樹堆” “Tree + Heap”
性質:每個點隨機分配一個權值,使treap同時滿足堆性質和二叉搜尋樹性質複雜度:期望O( logn )
作用:在logn的複雜度求出一個數的排名和排名為x的數或求一個數的前驅和後驅


正片開始:

以下關於treap的常識借鑑lxl的課件

設每個節點的關鍵字是key,隨機權值是rand
1.如果v是u的左兒子,則key[v] < key[u]
2.如果v是u的右兒子,則key[v] > key[u]
3.如果v是u的子節點,則rand[u] > rand[v]

Treap維護權值的時候一般會把相同的權值放在同一個節點上
• 所以一個treap節點需要維護以下資訊:
• 左右兒子
• 關鍵字
• 關鍵字出現次數
• 堆隨機值
• 節點大小(即子樹大小)

顯然lxl的課件是個人都讀不懂(除非你學過treap)

接下來是我的理解:

首先我們針對一個數x,將這個數看做二叉樹上的一個節點i它的關鍵字key為x,rand為一個隨機數(為了維護treap的平衡性,後面會講)。對於一個數列cnt[i]記錄key[i]出現的次數,siz[i]記錄以節點i為根的樹的大小。
son[i][0]記錄i節點的左子樹根編號,son[i][1]記錄i節點右子樹的根節點編號。treap必須滿足左子樹中所有節點的key都小於當前樹根的key,右子樹中所有的key都大於當前樹根的key。
對於一個數列中的數x,x的排名為小於它的數的個數+1,則對於treap上的一個節點i它的排名為上面的樹+左子樹大小(這個程式碼中詳細,這裡不做過多敘述),前驅,後驅後面詳細講。
好了treap大概的思路就說完了

yzh表達能力又nb了


程式碼講解部分:

注:treap中的一些操作關係到樹根節點編號的改變請務必加上&符號

push_up:

首先對於任何維護的樹中push_up不可或缺:

void push_up(int x) {
	siz[x] = siz[son[x][0]] + siz[son[x][1]] + cnt[x];//以x節點為根的樹的大小為左子樹大小+右子樹大小+當前key出現的次數
}

神奇的旋轉:

然後就是對於treap的旋轉操作:

許多oier很疑惑為什麼要旋轉?什麼是旋轉?
如下圖


這是一個以4節點為目標節點進行的右旋操作,反之就是左旋操作,不難發現這麼旋轉過後treap的性質並沒有改變,反而可以利用這種旋轉操作來進行一些方便我們的操作
下面是程式碼:

void rotate(int& x, int y) {//x為要進行旋轉的節點,y為1時右旋y為0時左旋
	int ii = son[x][y ^ 1];
	son[x][y ^ 1] = son[ii][y];
	son[ii][y] = x;
	push_up(x);
	push_up(ii);
	x = ii;
}

這段程式碼不用我過多解釋了吧,多仔細考慮考慮就能懂,學treap的oier這些應該都能理解,有不明白的自行百度。

接下來是新增節點操作:

思路:按照treap中一個節點的左子樹key都比當前key小,右子樹key都比當前key大的規律找到一個合適的葉節點位置將當前要插入的x新增,然後按照上面說的各個變數進行復制,並生產一個隨機數用來維護平衡(後面會講)。
例如在下面這個圖中插入一個key為0的節點:

我們從根節點按照規律往下找知道發現一個可以放的葉子節點空位(為1的左子樹根)。
如下圖:

那如果插入的數之前已經有過怎麼辦,這就好說了,如果數中已經存在要插入的樹x,那麼在按treap規律往下找節點的過程中必然會經過key為x的節點
這時只需要將當前節點cnt++就可以了(此思路比較簡單),直接上程式碼:
注:在此程式碼中x為當前節點編號,y為要插入的數值,不要與上面的思路描述搞混

void ins(int& x, int y) {
	if (!x) {//按照treap規律找到最低端符合條件的葉子節點
		x = ++sz;
		key[x] = y;
		cnt[x] = siz[x] = 1;
		rd[x] = rand();
		return;
	}
	if (key[x] == y) {//若要插入的樹以存在
		cnt[x]++;
		siz[x]++;
		return;
	}
	int t = (y > key[x]);//若要插入的數大於當前key則從右子樹找,反之
	ins(son[x][t], y);
	if (rd[x] > rd[son[x][t]])//隨機數維護操作(看下面的解釋)
		rotate(x, t ^ 1);
	push_up(x);
}

許多oier對於rd隨機數的維護還是有點疑惑,那我就說一下我對於隨機數維護的看法:
前面我們講過對於treap中的任意一個節點進行旋轉操作treap的性質都不會改變,然而在下面我們要講到的查詢操作中頻繁呼叫旋轉操作,可能會導致treap變成一條鏈,這樣就不能稱為平衡樹了,所以我們這裡就用隨機數維護,認真觀察的oier不難發現這裡的維護是用的小頂堆維護(當然大頂堆也可以),因為隨機數具有隨機性所以這樣維護起來的是完全沒有問題的,這樣就可以使treap保持基本穩定狀態。
(以上只是我對與隨機數維護的個人看法,不一定對,若有疑問可百度或評論留言)
下面是lxl對ins的看法(有興趣的可以看看,我不認為他講的比我細 ):

• 先給這個節點分配一個隨機的堆權值
• 然後把這個節點按照bst的規則插入到一個葉子上:
• 從根節點開始,逐個判斷當前節點的值與插入值的大小關係。如
果插入值小於當前節點值,則遞迴至左兒子;大於則遞迴至右兒
子;
• 然後通過旋轉來調整,使得treap滿足堆性質

delete 刪除操作:

這個和新增有點相似
其實就是按照treap的定義往下查詢找到要刪除的數後一直將這個節點用旋轉操作一直往下探,直到這個節點為葉子節點時即可進行刪除操作,這裡往下探的時候用的是貪心策略,注意維護小頂堆。
有的人會問為什麼要將這個節點旋轉到葉子節點呢?
因為如果在中間進行旋轉操作那麼可能會導致中間的節點消失,從而造成樹裂開的情況。
由於思路比較簡單這裡yzh就不做過多說明若有不懂的看參考lxl的思路:

• 和普通的BST刪除一樣:
• 如果刪除值小於當前節點值,則遞迴至左兒子;大於則遞迴至右
兒子
• 若當前節點數值的出現次數大於 1 ,則減一(通常將同一個權
值縮掉)
• 若當前節點數值的出現次數等於 1 :
• 若當前節點沒有左兒子與右兒子,則直接刪除該節點(置 0);
• 若當前節點沒有左兒子或右兒子,則將左兒子或右兒子替代該節
點;
• 若當前節點有左兒子與右兒子,則不斷旋轉當前節點,並走到當
前節點新的對應位置,直到沒有左兒子或右兒子為止。

程式碼:

void del(int& x, int y) {
	if (!x) {
		return;
	}
	if (key[x] != y) {
		del(son[x][y > key[x]], y);
	}
	else if (key[x] == y) {
		if (!son[x][0] && !son[x][1]) {
			cnt[x]--;
			siz[x]--;
			if (!cnt[x]) {
				x = 0;
			}
		}
		else if (son[x][0] && !son[x][1]) {
			rotate(x, 1);
			del(son[x][1], y);
		}
		else if (!son[x][0] && son[x][1]) {
			rotate(x, 0);
			del(son[x][0], y);
		}
		else {
			int t = (rd[son[x][0]] < rd[son[x][1]]);//小頂堆維護
			rotate(x, t);
			del(son[x][t], y);
		}
	}
	push_up(x);
	
}

這麼詳細的講解再聽不懂就得remake
注:以下任何查詢操作中都沒有更改根節點的操作所以不能再加&
查詢操作lxl寫的不詳細所以就不再展示lxl的思路了

查詢排名

常規先從treap的樹根出發按照treap左邊小右邊大的規律往下找,直到找到需要查詢的樹時,返回排名的累計值,如果查到葉子節點還是沒有找到要查詢的數的話就更具題目具體要求輸出(比如-1),重點在於怎麼求累計值+1(一個數的排名是小於這個數的個數+1)。
當走到一個點i的時候若要查詢的數x小於key[i]那麼不累計並往左子樹找x,若x大於key[i]那麼將排名加上左子樹的大小與cnt[i]的值(因為左子樹的數一定都是小於x,key[i]也小於x,這個操作就是文章開頭所說的i節點上面的樹的大小)並往右邊找。
此思路比較簡單,直接上程式碼:

int get_rank(int x, int y) {
	if (!x) return 0;
	if (key[x] == y) {
		return siz[son[x][0]] + 1;
	}
	if (key[x] < y) {
		return siz[son[x][0]] + cnt[x] + get_rank(son[x][1], y);
	}
	return get_rank(son[x][0], y);
}

查詢排名為x的數

這裡查排名就不能按照常規了,查排名是按照排名累計值來找。

走到節點i時判斷要查詢的排名x是否<=siz[son[i][0]] 的值

若成立則證明排名為x的值在以i節點為根節點樹的左子樹

若不成立且siz[son[i][0]] + cnt[i] < x則證明在右子樹,那麼我們就將x減去siz[son[i][0]] + cnt[i]並往右子樹找,這步減的操作是將要查詢的x排名減去當前累計排名值(思考片刻遍能理解)

若發現上面兩個條件都不滿足,則確定siz[son[i][0]] < x <= siz[son[i][0]] +cnt[i],就證明排名為x的數就是key[i],直接返回key[i]

若按照以上規律找到葉子節點還是沒找到排名為x的數,則不存在排名為x的數,此情況按照題目給定要求輸出(如-1)

程式碼:

int find(int x, int y) {//x為當前節點編號,y表示查詢排名為y的值,與上文有所不同
	if (!x) return 0;
	if (siz[son[x][0]] >= y) {
		return find(son[x][0], y);
	}
	else if (siz[son[x][0]] + cnt[x] < y) {
		return find(son[x][1], y - siz[son[x][0]] - cnt[x]);
	}
	else return key[x];
}

剩下的查詢操作就比較簡單了,自己看看就能讀懂(其實是我懶得寫了) 。
這裡粘一道題
洛谷P3369 模版題
可對照模版體自行學習下面程式碼,日後有時間yzh會再進行補充

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#pragma warning(disable:4996)
using namespace std;
int T,son[100010][4],sz,cnt[100010],siz[100010],key[100010],rd[1000010],rt,tot;
void push_up(int x) {
	siz[x] = siz[son[x][0]] + siz[son[x][1]] + cnt[x];//以x節點為根的樹的大小為左子樹大小+右子樹大小+當前key出現的次數
}
void rotate(int& x, int y) {
	int ii = son[x][y ^ 1];
	son[x][y ^ 1] = son[ii][y];
	son[ii][y] = x;
	push_up(x);
	push_up(ii);
	x = ii;
}
void ins(int& x, int y) {
	if (!x) {//按照treap規律找到最低端符合條件的葉子節點
		x = ++sz;
		key[x] = y;
		cnt[x] = siz[x] = 1;
		rd[x] = rand();
		return;
	}
	if (key[x] == y) {//若要插入的樹以存在
		cnt[x]++;
		siz[x]++;
		return;
	}
	int t = (y > key[x]);//若要插入的數大於當前key則從右子樹找,反之
	ins(son[x][t], y);
	if (rd[x] > rd[son[x][t]])//隨機數維護操作
		rotate(x, t ^ 1);
	push_up(x);
}
void del(int& x, int y) {
	if (!x) {
		return;
	}
	if (key[x] != y) {
		del(son[x][y > key[x]], y);
	}
	else if (key[x] == y) {
		if (!son[x][0] && !son[x][1]) {
			cnt[x]--;
			siz[x]--;
			if (!cnt[x]) {
				x = 0;
			}
		}
		else if (son[x][0] && !son[x][1]) {
			rotate(x, 1);
			del(son[x][1], y);
		}
		else if (!son[x][0] && son[x][1]) {
			rotate(x, 0);
			del(son[x][0], y);
		}
		else {
			int t = (rd[son[x][0]] < rd[son[x][1]]);//小頂堆維護
			rotate(x, t);
			del(son[x][t], y);
		}
	}
	push_up(x);
	
}
int get_rank(int x, int y) {
	if (!x) return 0;
	if (key[x] == y) {
		return siz[son[x][0]] + 1;
	}
	if (key[x] < y) {
		return siz[son[x][0]] + cnt[x] + get_rank(son[x][1], y);
	}
	return get_rank(son[x][0], y);
}
int find(int x, int y) {
	if (!x) return 0;
	if (siz[son[x][0]] >= y) {
		return find(son[x][0], y);
	}
	else if (siz[son[x][0]] + cnt[x] < y) {
		return find(son[x][1], y - siz[son[x][0]] - cnt[x]);
	}
	else return key[x];
}
int pre(int x, int y) {
	if (!x) return -0x3f3f3f3f;
	if (key[x] >= y) {
		return pre(son[x][0], y);
	}
	else {
		return max(key[x], pre(son[x][1], y));
	}
}
int nxt(int x, int y) {
	if (!x) return 0x3f3f3f3f;
	if (key[x] <= y) {
		return nxt(son[x][1], y);
	}
	else {
		return min(key[x], nxt(son[x][0], y));
	}
}
int main() {
	scanf("%d", &T);
	while (T--) {
		int opt, v;
		scanf("%d%d", &opt, &v);
		if (opt == 1) {
			ins(rt, v);	
		}
		else if (opt == 2) {
			del(rt, v);
		}
		else if (opt == 3) {
			printf("%d\n", get_rank(rt, v));
		}
		else if (opt == 4) {
			printf("%d\n", find(rt, v));
		}
		else if(opt==5){
			printf("%d\n", pre(rt, v));
		}
		else if (opt == 6) {
			printf("%d\n", nxt(rt, v));
		}
	}
	return 0;
}