平衡樹入門
定義與性質
平衡樹是二叉搜索樹和堆合並構成的一種數據結構,所以它的名字是 \(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;
}
【模板】普通平衡樹
模板題,給出的示例代碼與講解均圍繞此題展開。