普通平衡樹學習筆記之Splay演算法

liuchanglc 發表於 2020-07-14

前言

今天不容易有一天的自由學習時間,當然要用來“學習”。在此記錄一下今天學到的最基礎的平衡樹。

定義

平衡樹是二叉搜尋樹和堆合併構成的資料結構,它是一 棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。
這裡僅僅說明一下平衡樹中的\(Splay\)演算法

進入正題

平衡樹中有許多種類:紅黑樹、\(AVL\)樹,伸展樹,\(Treap\)等等,但是\(Splay\)演算法算是可用性很強的一種了。也就是說比較穩定。

\(Splay\)演算法中,一個處處都要用到的東西就是旋轉,即將當前節點與其前邊一個節點依次旋轉到目標位置。由於這個樹是一個二叉搜尋樹,所以旋轉之後要保證性質不變。我們就需要找到當前節點的父親和爺爺節點,然後先更新爺爺節點與當前節點之間的關係,然後將父親節點與該節點所屬關係的另一個子樹連起來,最後再處理一下該節點和父親節點的關係。

我們舉個例子(畢竟不太好理解)我們設這三個點分別為\(x,y,z\),從左往右分別是後一個的兒子,我們先把圖畫一下(\(x\)的子節點我隨便用一堆東西表示):
普通平衡樹學習筆記之Splay演算法
在這裡\(x\)\(y\)的左節點。那麼我們下一步就是把\(x\)變為\(z\)的子節點,也就是把\(y\)換下來。這一步很簡單,變成了下邊這樣:
普通平衡樹學習筆記之Splay演算法

然後就是關鍵了,因為把\(x\)旋轉上來,\(x\)也是有子節點的,所以我們需要進一步處理。首先記錄一下\(x\)\(y\)節點的左還是右兒子,在這個例子裡是左兒子,因為我們不能破壞這個樹的順序和性質,所以就需要讓\(y\)的右兒子不變且所有滿足性質的比\(x\)小的他的左兒子還是\(x\)的左兒子,而\(y\)的左兒子變成\(x\)的右兒子,也就是下邊這個圖:(剛剛沒有\(y\)的右節點,現在加上,編號與之前的不同,自己理解理解\(qwq\)):
普通平衡樹學習筆記之Splay演算法

類似的一個圖,這樣就完成了一次旋轉。最後不樣忘記也是需要\(pushup\)的,來維護點的兒子數量和自己的數量。
下邊放一下\(Splay\)和旋轉的程式碼

void rotate(int x){//旋轉
	int y=t[x].fa;
	int z=t[y].fa;
	int k=t[y].ch[1]==x;//找到x是y的左還是右節點,便於進行上文中所說操作
	t[z].ch[t[z].ch[1]==y]=x;//以下都是上邊說過的操作。
	t[x].fa=z;
	t[y].ch[k]=t[x].ch[k^1];
	t[t[x].ch[k^1]].fa=y;
	t[x].ch[k^1]=y;
	t[y].fa=x;
	pushup(y);
	pushup(x);
}
void splay(int x,int goal){//旋轉
	while(t[x].fa!=goal){//父親節點不是目標
		int y=t[x].fa;
		int z=t[y].fa;
		if(z!=goal){//爺爺節點也不是目標
			(t[y].ch[0]==x)^(t[z].ch[0]==x)?rotate(x):rotate(y);//其中一個點的左兒子是x就翻轉x,不然翻轉y(因為樹有序,不能破壞性質)
		}//上邊這句話還需要細細鑽研,本題解以後還會完善
		rotate(x);
	}
	if(goal == 0){//到了根節點的話讓根節點更新
		root = x;
	}
}

一些操作

上邊把\(Splay\)的操作說了一遍,也就是關鍵的旋轉應該比較清晰了,下邊我們就需要進行一些操作了。
平衡樹有許多可以進行的操作:刪除,插入,查詢一個值\(x\)的排名,查詢\(x\)排名的值,還有值\(x\)的前驅和後繼。(前驅定義為小於 \(x\),且最大的數,後繼定義為大於 \(x\)且最小的數)。這些都需要上邊的旋轉,所以旋轉明白了,這些也就比較容易了。首先是結構體的定義:

struct Node
{
       int ch[2];//子節點
       int fa;//父節點 
       int cnt;//數量
       int val;//值 
       int son;//兒子數量  
}t[maxn];

\(1\)、插入

我們肯定要從根節點開始向下找到符合這個值的點,或者新建一個點,那麼我們首先另\(u\)為根節點,其父親為\(0\),然後如果有根且插入的值不是當前節點的值,那麼我們就需要向子節點擴充套件,這裡我的擴充套件比較巧妙,因為兒子只有左右分別用\(0\)\(1\)表示,所以直接用判斷來找到底是左還是右,也就是這樣的式子:\(x>t[u].val\)。如果大於的話,就是右兒子。否則就是左兒子。所以直接更新到當前節點的兒子節點。
當跳出向下擴充套件的迴圈,就說明當前點值就是要插入的值,我們只需要將大小加一就行。如果沒有這樣的節點,那麼就新建一個節點,然後將父親的兒子節點置為當前節點,而左右兒子的判斷如上文所說。其餘的東西都是初始化,具體看程式碼,最後不要忘了再將當前節點\(Splay\)到根(幾乎每種操作都需要\(Splay\),查詢前驅後繼和排名的值不用):

void Add(int x){
	int u=root,fa=0;
	while(u&&t[u].val!=x){
		fa=u;
		u=t[u].ch[x>t[u].val];
	}
	if(u){//有的話直接大小加一
		t[u].cnt++;
	}
	else{//沒有該值的節點
		u=++tot;
		if(fa)t[fa].ch[x>t[fa].val]=u;
		t[tot].ch[1]=0;
		t[tot].ch[0]=0;
		t[tot].val=x;
		t[tot].fa=fa;
		t[tot].cnt=1;
		t[tot].son=1;
	}
	splay(u,0);
}

\(2\)、查詢值為\(x\)的排名:

根據這個判斷\(x>t[u].val\)依次找\(x\)的位置,最後\(Splay\)一下就好了:

void Find(int x){
	int u=root;//從根開始
	if(!u)return;//沒有樹就直接跳出
	while(t[u].ch[x>t[u].val] && x!=t[u].val){//依次向下找到當前值的點
		u=t[u].ch[x>t[u].val];//更新
	}
	splay(u,0);//旋轉
}

這個到最後把這個位置\(Splay\)到了根,所以答案就是當前\(Find\)之後根的左兒子的兒子數。

\(3\)、求前驅後繼(這個操作求出來的是節點編號)

我們首先需要找到排名,也就是操作\(2\),然後\(Splay\)到根節點,如果值大於當前值且查詢的是後繼或者小於當前且找前驅就直接返回,否則就向子節點轉移。找到轉移後最接近當前值的點,也就是說,如果第一次不滿足,假如找前驅就向左走一個,然後找到左兒子的右節點的最下邊的點,也就是最接近這個查詢的值的點。

int Fr_last(int x,int flag){//前驅flag為0,後繼為1
	Find(x);//找到位置
	int u=root;//根開始
	if((t[u].val>x&&flag) || (t[u].val<x && !flag)){//當前點滿足就直接返回
		return u;
	}
	u=t[u].ch[flag];//向目標點(左或右)轉移
	while(t[u].ch[flag^1])u=t[u].ch[flag^1];//找到轉移後最接近當前值的點
	return u;//返回
}

\(4\)、查詢排名為\(x\)的值

首先從根節點開始,如果一共都沒有\(x\)個數,那麼就直接返回\(0\),不然的話就分別記錄一下當前點的左右節點,然後判斷,如果當前點的子節點樹加上當前點的值的數量小於查詢的排名,直接減去然後走到右兒子,不然就走到左兒子就行了。

int Find_thval(int x){
	int u=root;//根開始
	if(t[u].son<x){//如果沒有這麼多,直接返回0
		return 0;
	}
	while(666666){//一直迴圈
		int y=t[u].ch[0];//記錄左兒子
		if(x>t[y].son+t[u].cnt){//排名大就減去,然後走到右節點
			x-=t[y].son+t[u].cnt;
			u=t[u].ch[1];
		}
		else{
			if(x<=t[y].son){//否則走到左節點
				u=y;
			}
			else return t[u].val;//如果排名比上邊的小,且比左節點的值大,這就是滿足的價值,直接返回
		}
	}
}

我的這個程式碼有一些等號的取捨不同,所以在查詢的時候傳遞引數需要加上一。

\(5\)、刪除

我們需要首先找出這個點的前驅和後繼,然後旋轉下去,要刪除的就是後繼的左兒子,假如這個點的數量大於\(1\),就直接數量減一就好了,然後翻轉到根節點,如果小於等於\(1\),那麼就把這個點變成\(0\),結束!

void Delete(int x){
	int Front=Fr_last(x,0);//前驅
	int Last=Fr_last(x,1);//後繼
	splay(Front,0);//旋轉
	splay(Last,Front);
	int del=t[Last].ch[0];//找到需要刪除的點
	if(t[del].cnt>1){//大於1直接減
		t[del].cnt--;
		splay(del,0);
	}
	else{//否則直接刪除
		t[Last].ch[0]=0;
	}
}

總結

以上就是\(Splay\)的一些實現和操作,以後部落格還會進行修改和完善,這些只是暫時自學時的理解,如果有神犇能給蒟蒻一些指導那就更好了。
完結撒花\(qwqq\)
下邊推薦一個板子題普通平衡樹板子

板子題程式碼:

細節的註釋上邊都寫過了,祝願大家學習愉快\(qwq\)

#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5+10;
const int Inf = 2147483647;
struct Node{
	int son,ch[2],fa,cnt,val;
}t[maxn];
int n,tot,root;
void pushup(int x){
	t[x].son = t[t[x].ch[0]].son+t[t[x].ch[1]].son+t[x].cnt;
}
void rotate(int x){
	int y=t[x].fa;
	int z=t[y].fa;
	int k=t[y].ch[1]==x;
	t[z].ch[t[z].ch[1]==y]=x;
	t[x].fa=z;
	t[y].ch[k]=t[x].ch[k^1];
	t[t[x].ch[k^1]].fa=y;
	t[x].ch[k^1]=y;
	t[y].fa=x;
	pushup(y);
	pushup(x);
}
void splay(int x,int goal){
	while(t[x].fa!=goal){
		int y=t[x].fa;
		int z=t[y].fa;
		if(z!=goal){
			(t[y].ch[0]==x)^(t[z].ch[0]==x)?rotate(x):rotate(y);
		}
		rotate(x);
	}
	if(goal == 0){
		root = x;
	}
}
void Find(int x){
	int u=root;
	if(!u)return;
	while(t[u].ch[x>t[u].val] && x!=t[u].val){
		u=t[u].ch[x>t[u].val];
	}
	splay(u,0);
}

void Add(int x){
	int u=root,fa=0;
	while(u&&t[u].val!=x){
		fa=u;
		u=t[u].ch[x>t[u].val];
	}
	if(u){
		t[u].cnt++;
	}
	else{
		u=++tot;
		if(fa)t[fa].ch[x>t[fa].val]=u;
		t[tot].ch[1]=0;
		t[tot].ch[0]=0;
		t[tot].val=x;
		t[tot].fa=fa;
		t[tot].cnt=1;
		t[tot].son=1;
	}
	splay(u,0);
}
int Fr_last(int x,int flag){
	Find(x);
	int u=root;
	if((t[u].val>x&&flag) || (t[u].val<x && !flag)){
		return u;
	}
	u=t[u].ch[flag];
	while(t[u].ch[flag^1])u=t[u].ch[flag^1];
	return u;
}
void Delete(int x){
	int Front=Fr_last(x,0);
	int Last=Fr_last(x,1);
	splay(Front,0);
	splay(Last,Front);
	int del=t[Last].ch[0];
	if(t[del].cnt>1){
		t[del].cnt--;
		splay(del,0);
	}
	else{
		t[Last].ch[0]=0;
	}
}
int Find_thval(int x){
	int u=root;
	if(t[u].son<x){
		return 0;
	}
	while(666666){
		int y=t[u].ch[0];
		if(x>t[y].son+t[u].cnt){
			x-=t[y].son+t[u].cnt;
			u=t[u].ch[1];
		}
		else{
			if(x<=t[y].son){
				u=y;
			}
			else return t[u].val;
		}
	}
}
int main(){
	int n;
	Add(Inf);
	Add(-Inf);
	scanf("%d",&n);
	while(n--){
		int opt;
		scanf("%d",&opt);
		int x;
		if(opt == 1){
			scanf("%d",&x);
			Add(x);
		}
		if(opt == 2){
			scanf("%d",&x);
			Delete(x);
		}
		if(opt == 3){
			scanf("%d",&x);
			Find(x);
			int ans = t[t[root].ch[0]].son;
			printf("%d\n",ans);
		}
		if(opt == 4){
			int ans;
			scanf("%d",&x);
			ans = Find_thval(x+1);
			printf("%d\n",ans);
		}
		if(opt == 5){
			scanf("%d",&x);
			int ans = Fr_last(x,0);
			printf("%d\n",t[ans].val);
		}
		if(opt == 6){
			scanf("%d",&x);
			int ans = Fr_last(x,1);
			printf("%d\n",t[ans].val);
		}
	}
	return 0;
}