資料結構:zyf樹/毒瘤樹
upd:把它叫成宗法樹被lxl罵了,現在改一下
ps:由於CSDN沒有替換功能,而且博主很懶,所以下面名字不改,大家清楚就好(逃
一看題目是不是很懵逼?
那就對了!
這個資料結構本來是沒有名字的,由一個毒瘤dalao發明並傳給講師,講師再交給我們。
至於這個名字,則是學員中一位毒瘤想到的。原因待會兒再講。
為了方便,以下就稱這種資料結構為“宗法樹”。
宗法樹是一種類似於平衡樹的資料結構,但似乎更簡單。它支援以下6種功能:
1、插入x
2、刪除x(若有多個x,只刪除一個)
3、求x的排名(嚴格比x小的數的個數+1)
4、求第x個數(同上)
5、求x的前驅(最大的小於x的數)
6、求x的後繼(最小的大於x的數)
以上6種功能均為平衡樹基本功能,但用程式碼量更小的宗法樹也可以實現。
宗法樹的性質:
1、是一棵二叉樹
2、它的數字都存在葉子節點裡
3、非葉子節點儲存子樹的最大值/最小值
4、每個非葉子節點的左子樹裡的數全都小於右子樹裡的數。
5、每個非葉子節點都必須有兩棵子樹
比如這樣一棵宗法樹:
而下面,我將按順序講解每一種功能的實現。
1、插入
以上面那棵宗法樹為例,如果我們想插入6:
1、從根節點出發,發現左兒子的值是5<6,於是向右兒子遞迴
2、發現5號節點是葉子節點,於是在5號節點下方再建兩個新節點6、7
3、5號節點原來的值9>6,所以將9放到右兒子,6放到左兒子
4、回溯更新
插入結果:
程式碼實現:
void insert(int &k,int x) { if (!k)//沒有這個點,即第一次插入 { new_tr(k,x);//動態開點 return; } if (leaf(k))//葉子節點 { new_tr(tr[k].lson,min(x,tr[k].v));//建左兒子 new_tr(tr[k].rson,max(x,tr[k].v));//建右兒子 push_up(k);//更新當前節點 return; } int l=tr[tr[k].lson].v;//左子樹最大值 if (x>l) insert(tr[k].rson,x);//大於左子樹最大值,向右遞迴 else insert(tr[k].lson,x);//向左遞迴 push_up(k);//更新當前節點 }
2、刪除節點
從上一次插入結果開始,以刪除5為例:
1、從根節點開始,左兒子2號節點的值5>=5,向左遞迴
2、2號節點的左兒子的值3<=5,向右遞迴
3、找到5,將4號節點刪除
4、刪除後2號節點只有左兒子,沒有右兒子,因此該節點沒有意義,用3號節點將其代替
5、將刪除的節點回收(可以沒有)
6、回溯更新
刪除結果:
程式碼實現(不帶回收功能):
void del(int k,int fa,int x)//fa是k的父節點
{
if (leaf(k))//葉子節點
{
if (tr[k].v==x)//找到x
{
if (tr[fa].lson==k) tr[fa]=tr[tr[fa].rson];//x是左兒子,用右兒子代替父節點
else tr[fa]=tr[tr[fa].lson];//x是右兒子,用左兒子代替父節點
}
return;
}
int l=tr[tr[k].lson].v;//左子樹最大值
if (x>l) del(tr[k].rson,k,x);//大於左子樹最大值,向右遞迴
else del(tr[k].lson,k,x);//向左遞迴
push_up(k);//更新當前節點
}
3、求x的排名
這個更容易實現,每次:
1、葉節點,值不比x小,返回1
2、葉節點,值比x小,返回2
2、x比左兒子值大,返回向右遞迴結果+左子樹size
3、x不比左兒子值大,返回向左遞迴結果
程式碼實現:
int rnk(int k,int x)
{
if (leaf(k))//葉節點
{
if (x>tr[k].v) return 2;//第二種
return 1;//第一種
}
int l=tr[tr[k].lson].v;//左子樹最大值
if (x>l) return rnk(tr[k].rson,x)+tr[tr[k].lson].sz;//第三種
return rnk(tr[k].lson,x);//第四種
}
4、求排名為x的數
也很容易。分三種可能:
1、葉節點,返回節點值
2、左子樹size<=x,結果在左子樹內,向左遞迴x
3、左子樹size>x,結果在右子樹內,向右遞迴(x-左子樹size)
程式碼實現:
int kth(int k,int rnk)
{
if (leaf(k)) return tr[k].v;//第一種
if (rnk<=tr[tr[k].lson].sz) return kth(tr[k].lson,rnk);//第二種
return kth(tr[k].rson,rnk-tr[tr[k].lson].sz);//第三種
}
5、求x的前驅
根據第3第4種功能,可以得出x前驅=kth(root,rnk(root,x)-1)
6、求x的後繼
同樣根據第3第4種功能,得出x後繼=kth(root,rnk(root,x+1))
附加優化:
由於加點刪點的方法,可能會出現有一棵子樹深度特別大,而另一邊特別小的狀況,導致遞迴時間過長,如下圖:
其中五角星代表下面還有深度巨大的子樹,顏色只是為了區分
對此,可以將一棵子樹上的一些資訊轉移到另一棵子樹上去,而同樣保證性質沒有被破壞,如上圖可以被轉化為下圖的狀態:
其中綠色邊是新增邊
程式碼實現如下:
void rotate(int k,bool dir)//自行理解
{
if (!dir)
{
int r=tr[k].rson;
tr[k].rson=tr[k].lson;
tr[k].lson=tr[tr[k].rson].lson;
tr[tr[k].rson].lson=tr[tr[k].rson].rson;
tr[tr[k].rson].rson=r;
push_up(tr[k].lson);
push_up(tr[k].rson);
}
else
{
int l=tr[k].lson;
tr[k].lson=tr[k].rson;
tr[k].rson=tr[tr[k].lson].rson;
tr[tr[k].lson].rson=tr[tr[k].lson].lson;
tr[tr[k].lson].lson=l;
push_up(tr[k].lson);
push_up(tr[k].rson);
}
}
void maintain(int k)
{
if (leaf(k)) return;//是葉子節點,不用修改
if (tr[tr[k].lson].sz>=tr[tr[k].rson].sz*4) rotate(k,0);//左兒子太重
if (tr[tr[k].rson].sz>=tr[tr[k].lson].sz*4) rotate(k,1);//右兒子太重
}
例題:洛谷 P3369 模板平衡樹
AC程式碼:
#include<bits/stdc++.h>
using namespace std;
struct hh
{
int v,sz,lson,rson;
}tr[1010101];
int cnt,n,root;
bool leaf(int x){return !(tr[x].lson||tr[x].rson);}
void new_tr(int &k,int x)
{
k=++cnt;
tr[k].v=x;
tr[k].sz=1;
}
void push_up(int x)
{
if (leaf(x)) return;
tr[x].v=max(tr[tr[x].lson].v,tr[tr[x].rson].v);
tr[x].sz=tr[tr[x].lson].sz+tr[tr[x].rson].sz;
}
void rotate(int k,bool dir)//自行理解
{
if (!dir)
{
int r=tr[k].rson;
tr[k].rson=tr[k].lson;
tr[k].lson=tr[tr[k].rson].lson;
tr[tr[k].rson].lson=tr[tr[k].rson].rson;
tr[tr[k].rson].rson=r;
push_up(tr[k].lson);
push_up(tr[k].rson);
}
else
{
int l=tr[k].lson;
tr[k].lson=tr[k].rson;
tr[k].rson=tr[tr[k].lson].rson;
tr[tr[k].lson].rson=tr[tr[k].lson].lson;
tr[tr[k].lson].lson=l;
push_up(tr[k].lson);
push_up(tr[k].rson);
}
}
void maintain(int k)
{
if (leaf(k)) return;//是葉子節點,不用修改
if (tr[tr[k].lson].sz>=tr[tr[k].rson].sz*4) rotate(k,0);//左兒子太重
if (tr[tr[k].rson].sz>=tr[tr[k].lson].sz*4) rotate(k,1);//右兒子太重
}
void insert(int &k,int x)
{
if (!k)//沒有這個點,即第一次插入
{
new_tr(k,x);//動態開點
return;
}
if (leaf(k))//葉子節點
{
new_tr(tr[k].lson,min(x,tr[k].v));//建左兒子
new_tr(tr[k].rson,max(x,tr[k].v));//建右兒子
push_up(k);//更新當前節點
return;
}
int l=tr[tr[k].lson].v;//左子樹最大值
if (x>l) insert(tr[k].rson,x);//大於左子樹最大值,向右遞迴
else insert(tr[k].lson,x);//向左遞迴
push_up(k);//更新當前節點
}
void del(int k,int fa,int x)//fa是k的父節點
{
if (leaf(k))//葉子節點
{
if (tr[k].v==x)//找到x
{
if (tr[fa].lson==k) tr[fa]=tr[tr[fa].rson];//x是左兒子,用右兒子代替父節點
else tr[fa]=tr[tr[fa].lson];//x是右兒子,用左兒子代替父節點
}
return;
}
int l=tr[tr[k].lson].v;//左子樹最大值
if (x>l) del(tr[k].rson,k,x);//大於左子樹最大值,向右遞迴
else del(tr[k].lson,k,x);//向左遞迴
push_up(k);//更新當前節點
}
int rnk(int k,int x)
{
if (leaf(k))//葉節點
{
if (x>tr[k].v) return 2;//第二種
return 1;//第一種
}
int l=tr[tr[k].lson].v;//左子樹最大值
if (x>l) return rnk(tr[k].rson,x)+tr[tr[k].lson].sz;//第三種
return rnk(tr[k].lson,x);//第四種
}
int kth(int k,int rnk)
{
if (leaf(k)) return tr[k].v;//第一種
if (rnk<=tr[tr[k].lson].sz) return kth(tr[k].lson,rnk);//第二種
return kth(tr[k].rson,rnk-tr[tr[k].lson].sz);//第三種
}
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
int opt,x;
scanf("%d %d",&opt,&x);
if (opt==1) insert(root,x);
if (opt==2) del(root,0,x);
if (opt==3) printf("%d\n",rnk(root,x));
if (opt==4) printf("%d\n",kth(root,x));
if (opt==5) printf("%d\n",kth(root,rnk(root,x)-1));
if (opt==6) printf("%d\n",kth(root,rnk(root,x+1)));
}
}
//由於其刪除時父死兄繼和旋轉時很像過繼的特點,稱其為宗法樹。
upd:由於被lxl罵了,將該句註釋掉(逃