平衡樹入門


平衡樹入門

定義與性質

平衡樹是二叉搜索樹和堆合並構成的一種數據結構,所以它的名字是 \(tree(\)二叉搜索樹\()+heap(\)\()\)\(treap\)

事實上,堆和樹的性質是沖突的,二叉搜索樹要求滿足左兒子小於根節點小於右兒子,而堆是滿足根節點小於等於(或大於等於)左右兒子。因此在 \(treap\) 中,並不能以單一的鍵值作為節點的數據域。

\(treap\) 中的每個節點包含兩個值,我們設其為 \(val\)\(key\)

  • \(val\):滿足二叉搜索樹的性質。

  • \(key\):隨機生成,滿足堆的性質,即優先級。

簡單理解的話,平衡樹就是在二叉搜索樹上增加了一個 \(key\) 值,我們需要維護它滿足堆的性質。

如圖,即一個標准的 \(treap\)

\(fhq treap\)

ps:又稱無旋 \(treap\) ,編程復雜度優於 \(splay\)

節點信息

struct node{ 
	int l,r; //左右兒子 
	int val; //點權 
	int siz; //子樹大小 
	int key; //treap隨機值 
}tr[N];

\(fhq\) 的節點信息和普通 \(treap\) 並無本質上的差別,但沒有記錄相同權值節點的個數,即不能把有相同權值的節點當成一個點來處理。這一個差異給 \(fhq\) 空間上的浪費,但卻降低了編程的難度。

創建新節點

inline int build(int val)
{
	tr[++tot].val=val,tr[tot].key=random(INF);
	tr[tot].l=tr[tot].r=0,tr[tot].siz=1;
	return tot;
}

其中 \(tot\) 記錄節點的總量,\(val\) 是新節點的權值, \(rondom\) 函數返回一個隨機值。

合並信息

inline void pushup(int k) { tr[k].siz=tr[tr[k].l].siz+tr[tr[k].r].siz+1; }

分裂

inline void split(int k,int val,int &x,int &y)
{
	if(!k) { x=y=0; return; }
	if(tr[k].val<=val) x=k,split(tr[k].r,val,tr[k].r,y);
	else y=k,split(tr[k].l,val,x,tr[k].l);
	pushup(k);
}

函數 \(split(k,val,x,y)\) 相當於在做一件這樣的事情:

  • 把以 \(k\) 為根的子樹按照權值進行分裂:

    • 權值小於等於 \(val\) 的會到以 \(x\) 為根的子樹中。

    • 權值大於 \(val\) 的回到以 \(y\) 為根的子樹中。

接下來我們關注一下它是如何進行分裂的:

  • 如果這個節點的權值是小於等於 \(val\) 的,說明節點 \(k\) 和節點 \(k\) 的左子樹都會被划分到子樹 \(x\) 上去,而 \(k\) 的右子樹還沒有被划分,那我們就需要再遞歸一下去划分 \(k\) 的右子樹。注意此處我們是帶引用的在進行遞歸,所以如果有要划分到 \(x\) 上的節點,直接把他掛上去即可。、

  • 大於 \(val\) 的情況同理,此處不再贅述。

當然,我們仍然可以通過大小進行分裂,根據題目不同的要求,兩種分裂方式都應該掌握,整體思路和按照權值分裂其實並無大的區別。

inline void split(int k,int s,int &x,int &y)
{
    if(!k) { x=y=0; return; }
    if(tr[tr[k].l].siz+1<=s) x=k,split(tr[k].r,s-tr[tr[k].l].siz-1,tr[x].r,y),pushup(x);
    else y=k,split(tr[k].l,s,x,tr[y].l),pushup(y);
}

合並

inline int merge(int x,int y)
{
	if(!x||!y) return x+y;
	if(tr[x].key>tr[y].key){
		tr[x].r=merge(tr[x].r,y),pushup(x);
		return x;
	}
	else{
		tr[y].l=merge(x,tr[y].l),pushup(y);
		return y;
	}
}

此段代碼實現的操作是將以 \(x\) 為根的子樹與以 \(y\) 為根的子樹合並,需要注意的是我們這里保證了以 \(x\) 為根的子樹的權值最大值小於以 \(y\) 為根的子樹的權值最小值。同時我們需要不斷維護優先級,因為有如上的性質,所以我們不用判斷節點權值的大小而可以直接進行合並,最后這段代碼返回的值是合並完兩棵子樹后的根節點。

考慮如何維護優先級:

  • 如果 \(x\) 的優先級大於 \(y\) 的優先級,那么 \(x\) 和它的左子樹我們就不需要動,需要處理的是 \(x\) 的右子樹和 \(y\) 的合並問題,遞歸處理即可。

  • 反之,\(y\) 的優先級大於 \(x\) 的優先級亦同理,我們仍然可以遞歸處理 \(y\) 的左子樹和 \(x\)

按照這個過程一直遞歸,當有一棵子樹為空,則返回 \(x+y\) ,顯然,不失其正確性。

ps:分裂和合並是 \(fhq\) \(treap\) 的關鍵操作,其它操作的實現均基於此且相對簡單。

插入

inline void insert(int val)
{
	int x,y;
	split(root,val-1,x,y);
	root=merge(merge(x,build(val)),y);
}

向平衡樹中插入一個權值為 \(val\) 的節點。

實現時,按照權值 \(val-1\) 進行分裂,分裂后,權值小於 \(val-1\) 的節點都在 \(x\) 子樹中,其它節點在 \(y\) 子樹中,先把 \(x\) 和新建的節點合並,再合並整棵樹。

不難理解這個過程,實際上是為了保證我們合並時需要滿足的大小性質。

刪除

inline void delet(int val)
{
	int x,y,z;
	split(root,val,x,z),split(x,val-1,x,y);
	if(y) y=merge(tr[y].l,tr[y].r);
	root=merge(merge(x,y),z);
}

不難發現在分裂之后,以 \(y\) 為根的子樹里只有權值等於 \(val\) 的節點,合並左右子樹,並刪除根即可。

刪除完成后,將整棵樹重新合並。

查詢排名

inline int getrank(int val)
{
	int x,y,ans;
	split(root,val-1,x,y);
	ans=tr[x].siz+1;
	root=merge(x,y);
	return ans;
}

某個數的排名實際上就是比他小的數的個數 \(+1\),分裂后直接查 \(x\) 子樹的大小即可。

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

inline int getval(int rank)
{
	int k=root;
	while(k){
		if(tr[tr[k].l].siz+1==rank) break;
		else if(tr[tr[k].l].siz>=rank) k=tr[k].l;
		else rank-=tr[tr[k].l].siz+1,k=tr[k].r;
	}
	return !k?INF:tr[k].val;
}

由於我們的平衡樹滿足二叉搜索樹的性質,我們可以在上面進行一個類似於二分的過程,基於此展開討論即可。

前驅和后繼

inline int getpre(int val)
{
	int x,y,k,ans;
	split(root,val-1,x,y);
	k=x;
	while(tr[k].r) k=tr[k].r;
	ans=tr[k].val;
	root=merge(x,y);
	return ans;
}
inline int getnext(int val)
{
	int x,y,k,ans;
	split(root,val,x,y);
	k=y;
	while(tr[k].l) k=tr[k].l;
	ans=tr[k].val;
	root=merge(x,y);
	return ans;
}

查詢前驅就按照 \(val-1\) 分裂整棵樹,然后取 \(x\) 子樹最靠右的節點,后繼同理,不再贅述。

優化

由於一定程度上 \(fhq\) 會浪費空間,基於此的一個優化是將被刪除的節點用一個棧保存,新插入節點優先使用之前被刪去的節點,示例代碼中並沒有這個操作,相信並不難實現。

CODE

#include <bits/stdc++.h>
using namespace std;
const int N=5e5+10,INF=1e9;
inline int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w*=-1; ch=getchar(); }
	while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
	return s*w;
}
int n;
struct node{ int l,r,val,siz,key; }tr[N];
inline int random(int lim) { return rand()*rand()%lim+1; }
struct Treap{ //fhq平衡樹 
	int tot,root;
	inline void pushup(int k) { tr[k].siz=tr[tr[k].l].siz+tr[tr[k].r].siz+1; }
	inline int build(int val){
		tr[++tot].val=val,tr[tot].key=random(INF);
		tr[tot].l=tr[tot].r=0,tr[tot].siz=1;
		return tot;
	}
	inline void split(int k,int val,int &x,int &y){
		if(!k) { x=y=0; return; }
		if(tr[k].val<=val) x=k,split(tr[k].r,val,tr[k].r,y);
		else y=k,split(tr[k].l,val,x,tr[k].l);
		pushup(k);
	}
	inline int merge(int x,int y){
		if(!x||!y) return x+y;
		if(tr[x].key>tr[y].key){
			tr[x].r=merge(tr[x].r,y),pushup(x);
			return x;
		}
		else{
			tr[y].l=merge(x,tr[y].l),pushup(y);
			return y;
		}
	}
	inline void insert(int val){
		int x,y;
		split(root,val-1,x,y);
		root=merge(merge(x,build(val)),y);
	}
	inline void delet(int val){
		int x,y,z;
		split(root,val,x,z),split(x,val-1,x,y);
		if(y) y=merge(tr[y].l,tr[y].r);
		root=merge(merge(x,y),z);
	}
	inline int getrank(int val){
		int x,y,ans;
		split(root,val-1,x,y);
		ans=tr[x].siz+1,root=merge(x,y);
		return ans;
	}
	inline int getval(int rank){
		int k=root;
		while(k){
			if(tr[tr[k].l].siz+1==rank) break;
			else if(tr[tr[k].l].siz>=rank) k=tr[k].l;
			else rank-=tr[tr[k].l].siz+1,k=tr[k].r;
		}
		return !k?INF:tr[k].val;
	}
	inline int getpre(int val){
		int x,y,k,ans;
		split(root,val-1,x,y),k=x;
		while(tr[k].r) k=tr[k].r;
		ans=tr[k].val,root=merge(x,y);
		return ans;
	}
	inline int getnext(int val){
		int x,y,k,ans;
		split(root,val,x,y),k=y;
		while(tr[k].l) k=tr[k].l;
		ans=tr[k].val,root=merge(x,y);
		return ans;
	}
}treap;
int main()
{
	n=read();
	for(register int i=1;i<=n;i++){
		int opt=read(),x=read();
		if(opt==1) treap.insert(x);
		else if(opt==2) treap.delet(x);
		else if(opt==3) printf("%d\n",treap.getrank(x));
		else if(opt==4) printf("%d\n",treap.getval(x));
		else if(opt==5) printf("%d\n",treap.getpre(x));
		else if(opt==6) printf("%d\n",treap.getnext(x));
	}
	return 0;
}

\(splay treap\)

ps:通過旋轉維護的 \(treap\) ,一些操作與 \(LCT\) 有聯系。

節點信息

struct node{ 
	int fa; //父親節點編號 
	int val; //節點權值 
	int siz; //子樹大小 
	int cnt; //與該節點權值相同的數的個數 
}tr[N];
int son[N][2]; //son[i][0/1]記錄節點i的左右兒子

\(fhq\) 靠分裂和合並維護堆的性質避免二叉查找樹退化為一條鏈,而 \(splay\) 不需要 \(key\) 值,它通過每次操作后的 \(splay\) 讓樹趨於平衡,以此避免復雜度的退化。

合並信息

inline void pushup(int k) { tr[k].siz=tr[k].cnt+tr[son[k][0]].siz+tr[son[k][1]].siz; } 

創建新節點

inline void build()
{
	++tot;
	tr[tot].fa=son[tot][1]=son[tot][0]=tr[tot].cnt=tr[tot].siz=0;
	return tot;
}

ps:以上兩段代碼過於簡單,不加贅述。

左旋與右旋

正如分裂和合並是 \(fhq\) 最重要的操作, \(splay\) 的核心操作是兩種旋轉以及 \(splay\) 函數。

首先我們來看一個簡單的例子:

如圖所示的一個 \(treap\) 擁有三個節點,其中,根的右兒子是我們新插入的節點。

假設我們想要讓 \(treap\) 滿足大根堆的性質,那么我們需要在不改變 \(key\) 值順序的情況下,對節點進行變形,使得 \(key\) 滿足性質。

這一步即是我們的旋轉,對於如上示例,旋轉后的形態應該變為:

根據旋轉的不同,我們將旋轉分為兩種:左旋和右旋。

在例子中,我們是將右兒子節點旋轉至根,所以稱為左旋。反之,將左兒子節點旋轉至根,稱為右旋。

這個旋轉的具體過程,我們可以對應旋轉前后的圖進行分析,首先是左旋操作:

其過程具體如下:

  • 獲取根節點 \(A\) 的右兒子節點 \(B\)

  • 將節點 \(B\) 的父親節點信息更新為 \(f\) ,並更新節點 \(f\) 的子節點信息為 \(B\)

  • 將節點 \(A\) 的右兒子信息更新為 \(B\) 的左兒子 \(D\) ,同時將節點 \(D\) 的父親節點信息更新為 \(A\)

  • 將節點 \(B\) 的左兒子信息更改為節點 \(A\) ,同時將節點\(A\) 的父親節點信息更改為 \(B\)

接下來是右旋操作,其過程和左旋操作互為鏡像:

  • 獲取根節點 \(A\) 的左兒子節點 \(B\)

  • 將節點 \(B\) 的父親節點信息更新為 \(f\) ,並更新節點f的子節點信息為 \(B\)

  • 將節點 \(A\) 的左兒子信息更新為節點B的右兒子 \(D\),同時將節點 \(D\) 的父親節點信息更新為 \(A\)

  • 將節點 \(B\) 的右兒子信息更改為節點 \(A\) ,同時將節點 \(A\) 的父親節點信息更改為 \(B\)

我們考慮用一個函數實現這個過程:

inline void rotate(int k)
{
	int f1=tr[k].fa,f2=tr[f1].fa,opt=get(k);
	if(!f1) return;
	if(f2) son[f2][get(f1)=u;
	son[f1][opt]=son[k][!opt],tr[son[k][!opt]].fa=f1;
	son[k][!opt]=f1;
	tr[k].fa=f2;
	tr[f1].fa=k;
	pushup(f1),pushup(k); 
}

其中, \(get(k)\) 返回值為 \(1\) 表示當前節點是父親的右兒子,反之是左兒子,具體操作如下:

inline int get(int k) { return (son[tr[k].fa][1]==x);  }

帶入到上述過程,正確性顯然。

\(splay\)

回看我們的旋轉函數,每次進行這個操作,會讓我們的左右子樹中一棵高度 \(-1\) ,另一棵高度 \(+1\)

而我們的 \(splay\) 函數會通過一系列的調用,避免二叉查找樹退化成鏈。

我們每次查詢一個點,都將它 \(splay\) 到根,每次旋轉前關注當前節點父親節點的方向,如果方向一致就旋轉父親,這波操作的意義可以理解為盡量讓樹不規整,不然可能出現鏈的情況,否則旋轉當前節點。

對於該函數的正確性,有詳細的復雜度分析,但該博客的初衷在於學習平衡樹的算法且並非深入研究,故此我們略過這一部分,總而言之,我們可以認為 \(splay\) 過后的樹的高度期望在 \(logn\) 左右,該算法的時間復雜度均攤能夠達到 \(log\) 級別。

inline void splay(int k)
{
	while(tr[k].fa){
		int opt=(get(tr[k].fa)==get(k)); //判斷方向是否一樣 
		if(opt) rotate(tr[k].fa);
		else rotate(k);
		rotate(k); //再轉一下當前節點 
	}
	root=k;
}

插入

inline void insert(int k,int val,int fa)
{
	if(!k){
		k=build(); //新建一個節點 
		if(fa) tr[k].fa=fa;
		son[fa][tr[fa].val<val]=k;
		tr[k].cnt=tr[k].siz=1;
		tr[k].val=val;
		splay(k); //splay當前節點
		return; 
	}
	if(tr[k].val==val) { tr[k].cnt++; tr[k].siz++; splay(k); return; }
	insert(son[k][tr[k].val<val],val,k); 
}

插入操作即按部就班的寫即可,注意我們維護的點權中包括與當前點點權相同的點的個數,所以如果在遍歷的過程中成功碰到了點權與插入點權相同的節點,我們就不需要再新開節點,直接加在已有節點上並更新即可。

合並

inline void merge(int x,int y)
{
	if(!x||!y) { root=x+y; return; }
	int k=x;
	while(k) x=k,k=son[k][1];
	splay(x);
	son[x][1]=y,tr[y].fa=x;
	pushup(x);
}

\(merge(x,y)\) 表示合並以 \(x\) 為根和以 \(y\) 為根的兩棵子樹,其中保證子樹 \(x\) 的最大值小於子樹 \(y\) 的最小值。

其操作和 \(fhq\) 十分類似,找到子樹 \(x\) 最右鏈的末端,然后將其 \(splay\) 至根,然后直接將子樹 \(y\) 接在 \(x\) 的右子樹下即可。

刪除

inline void delet(int k,int val)
{
	if(tr[k].val==val){
		splay(k);
		tr[k].cnt--,tr[k].siz--;
		if(!tr[k].cnt){
			tr[son[k][0]].fa=tr[son[k][1]].fa=0;
			merge(son[k][0],son[k][1]); //合並兩棵子樹 
		}
		return;
	}
	delet(son[k][tr[k].val<val],val);
	pushup(k);
}

刪除操作也是直接進行即可,但當當前節點被刪空時,不要忘了合並當前節點的兩個子樹。

查詢排名

inline int getrank(int k,int val)
{
	if(tr[k].val==val){
		splay(k);
		return tr[son[k][0]].siz+1;
	} 
	return getrank(son[k][tr[k].val<val],val);
}

找到該點,將其 \(splay\) 至根,左子樹的大小即比它小的數的個數,其排名比之多 \(1\)

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

inline int getval(int k,int val)
{
	if(tr[son[k][0]].siz<val&&tr[son[k][0]].siz+tr[k].cnt>=val){
		splay(k);
		return tr[k].val;
	}
	if(tr[son[k][0]].siz>=val) return getval(son[k][0],val);
	return getval(son[k][1],x-tr[son[k][0]].siz=tr[k].cnt);
}

直接向下找即可,找到后別忘了 \(splay\)

前驅和后繼

inline int getpre(int val)
{
	getrank(root,val);
	int k=son[root][0],x=root;
	while(k) x=k,k=son[k][1];
	splay(x);
	return tr[x].val;
}
inline int getnext(int val)
{
	getrank(root,val);
	int k=son[root][1],x=root;
	while(k) x=k,k=son[k][0];
	splay(x);
	return tr[x].val;
}

操作比較簡單,把值為 \(val\) 的點 \(splay\) 到根后,暴力向下找即可。

細節

\(splay\) 在查詢排名、前驅、后繼時,需記得先插入查詢的數,否則,如果查詢的數載樹中不存在,將會導致運行錯誤。

CODE

#include <bits/stdc++.h>
using namespace std;
const int N=3e6;
inline int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w*=-1; ch=getchar(); }
	while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
	return s*w;
}
struct node{ 
	int fa; //父親節點編號 
	int val; //節點權值 
	int siz; //子樹大小 
	int cnt; //與該節點權值相同的數的個數 
}tr[N];
int n,tot,root; 
int son[N][2]; //son[i][0/1]記錄節點i的左右兒子 
//判斷是否為右兒子 
inline int get(int k) { return (son[tr[k].fa][1]==k);  }
inline void pushup(int k) { tr[k].siz=tr[k].cnt+tr[son[k][0]].siz+tr[son[k][1]].siz; } 
inline int build()
{
	++tot;
	tr[tot].fa=son[tot][1]=son[tot][0]=tr[tot].cnt=tr[tot].siz=0;
	return tot;
}
inline void rotate(int k)
{
	int f1=tr[k].fa,f2=tr[f1].fa,opt=get(k);
	if(!f1) return;
	if(f2) son[f2][get(f1)]=k;
	son[f1][opt]=son[k][!opt],tr[son[k][!opt]].fa=f1;
	son[k][!opt]=f1;
	tr[k].fa=f2;
	tr[f1].fa=k;
	pushup(f1),pushup(k); 
}
inline void splay(int k)
{
	while(tr[k].fa){
		int opt=(get(tr[k].fa)==get(k)); //判斷方向是否一樣 
		if(opt) rotate(tr[k].fa);
		else rotate(k);
		rotate(k); //再轉一下當前節點 
	}
	root=k;
}
inline void insert(int k,int val,int fa)
{
	if(!k){
		k=build(); //新建一個節點 
		if(fa) tr[k].fa=fa;
		son[fa][tr[fa].val<val]=k;
		tr[k].cnt=tr[k].siz=1;
		tr[k].val=val;
		splay(k); //splay當前節點
		return; 
	}
	if(tr[k].val==val) { tr[k].cnt++; tr[k].siz++; splay(k); return; }
	insert(son[k][tr[k].val<val],val,k); 
}
inline void merge(int x,int y)
{
	if(!x||!y) { root=x+y; return; }
	int k=x;
	while(k) x=k,k=son[k][1];
	splay(x);
	son[x][1]=y,tr[y].fa=x;
	pushup(x);
}
inline void delet(int k,int val)
{
	if(tr[k].val==val){
		splay(k);
		tr[k].cnt--,tr[k].siz--;
		if(!tr[k].cnt){
			tr[son[k][0]].fa=tr[son[k][1]].fa=0;
			merge(son[k][0],son[k][1]); //合並兩棵子樹 
		}
		return;
	}
	delet(son[k][tr[k].val<val],val);
	pushup(k);
}
inline int getrank(int k,int val)
{
	if(tr[k].val==val){
		splay(k);
		return tr[son[k][0]].siz+1;
	} 
	return getrank(son[k][tr[k].val<val],val);
}
inline int getval(int k,int val)
{
	if(tr[son[k][0]].siz<val&&tr[son[k][0]].siz+tr[k].cnt>=val){
		splay(k);
		return tr[k].val;
	}
	if(tr[son[k][0]].siz>=val) return getval(son[k][0],val);
	return getval(son[k][1],val-tr[son[k][0]].siz-tr[k].cnt);
}
inline int getpre(int val)
{
	getrank(root,val);
	int k=son[root][0],x=root;
	while(k) x=k,k=son[k][1];
	splay(x);
	return tr[x].val;
}
inline int getnext(int val)
{
	getrank(root,val);
	int k=son[root][1],x=root;
	while(k) x=k,k=son[k][0];
	splay(x);
	return tr[x].val;
}
int main()
{
	n=read();
	for(register int i=1;i<=n;i++){
		int opt=read(),x=read();
		if(opt==1) insert(root,x,0);
		else if(opt==2) delet(root,x);
		else if(opt==3) insert(root,x,0),printf("%d\n",getrank(root,x)),delet(root,x);
		else if(opt==4) printf("%d\n",getval(root,x));
		else if(opt==5) insert(root,x,0),printf("%d\n",getpre(x)),delet(root,x);
		else if(opt==6) insert(root,x,0),printf("%d\n",getnext(x)),delet(root,x);
	}
	return 0;
}

【模板】普通平衡樹

模板題,給出的示例代碼與講解均圍繞此題展開。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM