1. 程式人生 > >資料結構:zyf樹/毒瘤樹

資料結構: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罵了,將該句註釋掉(逃