平衡树学习笔记之 fhq Treap

平衡树学习笔记 1:fhq Treap(非旋 Treap)

正文开始前首先 %%% fhq 大佬。

众所周知,平衡树是一种 非常猥琐 码量堪忧的数据结构。
他的祖先是一种叫做二叉搜索树 ( \(BST\) ) 的东东。
话说二叉搜索树是个什么玩意呢?

二叉搜索树:

显而易见,这是一棵二叉树 (),它的每个节点上有一个需要我们维护的值,我们称为“关键码”
然后这棵树的中序遍历是一个关于关键码的一个严格单调递增序列 (假设没有重复的元素) 是有序的。
然后就没了。
当然我们可以用这玩意来搞很多非常 恶心 有用的东西。

平衡树:

根据二叉搜索树的性质,我们可以递归查询每一个点的排名,排名为 k 的点,还可以支持动态插入,删除某个数,它的每次操作的 期望复杂度 是\(O(log~n)\)因为一棵随机生成的BST的深度期望是 \(log~~n\)

但是我们操作二叉搜索树的时候发现了一些奇妙的东西:
比如说看这两张图:
平衡树学习笔记之 fhq Treap
平衡树学习笔记之 fhq Treap
显然前面这棵树它看起来更优一点,最后跑起来会比较快。
然后看一看极端数据,真是令人不寒而栗
平衡树学习笔记之 fhq Treap
你告诉我这和一条链有什么区别?
那么当你构造的BST 变成最后一个图这样,那你每次操作的复杂度最坏会达到 \(O(n)\)!
当然,我们发现有同样的BST 和这条链等效。
平衡树学习笔记之 fhq Treap
这两者是等效的,很明显后者得每次操作比前者更快,它的深度小。
所以我们把后者称为平衡树。
那么怎么维护一棵BST呢?

fhq Treap (非旋 Treap)

众所周知,有一种简单易懂的平衡树叫做 Treap,它在每一个点上弄了一个附加权值,这些附加权值满足 的性质,而维护的权值满足 BST 的性质。
Treap 是使用旋转操作来维护堆的性质的,但是我这个人真心菜,被绕的晕晕乎乎的。
然后看到了 fhq 非旋 treap,感觉简单易懂,操作思路清晰,功能强大跑的还不慢,
就愉快的去学了一下。
一般的 treap 的附加权值是怎么搞得?怎么样才能使树平衡呢?
当然是:随机!!!
所以fhq treap 借用了 treap 的随机化思想,只用了两个基本操作就能实现一般平衡树的所有操作。

基本操作1:分裂 (split)

它的主要思想就是把一个Treap分成两个。
split 操作有两种类型,一种是按照权值分配,一种是按前k个(数量)分配。
第一种就是把所有小于k的权值的节点分到一棵树中,第二种是把前k个点分到一个树里。
放上代码

权值版:

void split(ll now,ll k,ll &x,ll &y) //将以now为根的树按照 k 分裂为根为x,y的两部分
{
	if(now==0) x=y=0; //节点为空
	else
	{
		if(val[now]<=k) // 当前节点的值比 k 小,那么他的左子树上的所有值也一定比 k 小
		{
			x=now; //根节点成为现在的节点
			split(rc[now],k,rc[now],y); //递归看右子树
		}
		else // 同理
		{
			y=now;
			split(lc[now],k,x,lc[now]);
		}
		update(now); //更新答案
	}
}

对于我们遍历到每一个点,假如它的权值小于k,那么它的所有左子树,都要分到左边的树里,然后遍历它的右儿子。假如大于k,把它的所有右子树分到右边的树里,遍历左儿子。
一般我们用的比较多的是权值版。

前 k 个的版本:
有点像主席树找第 k 大的代码。

void split(ll now,ll k,ll &x,ll &y) //将以now为根的树按照前 k 个点分裂为根为x,y的两部分
{
	if(now==0) x=y=0; //节点为空
	else
	{
		if(siz[now]+1<=k) //这里的siz后面写成了f
		{
			x=now; //根节点成为现在的节点
			split(rc[now],k-siz[now]-1,rc[now],y); //递归看右子树
			//k要减去左子树记录的个数
		}
		else // 同理
		{
			y=now;
			split(lc[now],k,x,lc[now]);
		}
		update(now); //更新答案
	}
}

基本操作2:合并( merge )

将split分开的两棵平衡树 treap 合并起来。
因为第一个Treap的权值都比较小,我们比较一下它的 pos (附加权值),假如第一个的 pos 小,我们就可以直接保留它的所有左子树,接着把第一个 Treap 变成它的右儿子。反之,我们可以保留第二棵的所有右子树,指针指向左儿子。
你可以把这个过程形象的理解为在第一个Treap的左子树上插入第二个树,也可以理解为在第二个树的左子树上插入第一棵树。因为第一棵树都满足小于第二个树,所以就变成了比较pos来确定树的形态。
(转自 远航之曲 大佬)
代码很简洁:

ll make(ll x,ll y)
{
	if(x==0||y==0) return x+y; //返回其中有数的那棵树
	if(pos[x]<pos[y]) //pos 指的是优先级
	{
		rc[x]=make(rc[x],y);
		update(x);
		return x;
	}
	else 
	{
		lc[y]=make(x,lc[y]);
		update(y);
		return y;
	}
}

然后还有一堆平衡树的日常操作:

插入:

插入一个权值为v的点,把树按照v的权值split成两个,再按照顺序合并回去。

split(root,v,x,y);
root=make(make(x,getnew(v)),y);

删除:

稍微麻烦一点。
删除权值为v的点,把树按照v分成两个a,b,再把a按照v-1分成c,d。
把c的两个子儿子合并起来 (这时候那个点已经被删除了),再\(merge(merge(c,d),b)\),把分开的树合并回去。
有点绕自己好好理解。

split(root,v,x,z);
split(x,v-1,x,y);
y=make(lc[y],rc[y]);
root=make(make(x,y),z);

查找排名为 k 的数:

ll find(ll now,ll k) //now是树的根节点
{
	while(1+1==2)//c++划水写法
	{
		if(k<=f[lc[now]]) now=lc[now]; //比左子树要小
		else if(k==f[lc[now]]+1) return now;  //=左子树比他小的+1
		else k-=(f[lc[now]]+1),now=rc[now]; 
	}
}

查找前驱:

把root按v-1 split成x,y,在x里面找最大值。

split(root,v-1,x,y);
put(val[find(x,f[x])]); //1~v-1中最大的那个,也就是第f[x]的那个
root=make(x,y);

查找后继:

把root按 v split 成x,y,在y里面找最小值。

split(root,v,x,y);
put(val[find(y,1)]); //v+1~n 中的最小值
root=make(x,y);

查找 v 的排名:

把root按 v-1 split 成x,y,x 的 siz 大小+1

split(root,v-1,x,y);
put(f[x]+1); //左边都比现在的值小
root=make(x,y);

注意,对于在主函数中执行的 split 一般后面都会跟着相同数量的 megre (我写的是 make )

代码:

【洛谷 P3369】 普通平衡树

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<ctime>
#include<cstring>
#define r register
#define rep(i,x,y) for(r ll i=x;i<=y;++i)
#define per(i,x,y) for(r ll i=x;i>=y;--i)
using namespace std;
typedef long long ll;
const ll V=5e5+10;
ll t,x,y,k,z;
ll in()
{
	ll res=0,f=1;
	char ch;
	while((ch=getchar())<'0'||ch>'9')
	 if(ch=='-') f=-1;
	res=res*10+ch-48;
	while((ch=getchar())>='0'&&ch<='9')
	 res=res*10+ch-48;
	return res*f;
}
void put(ll x)
{
	if(x<0) putchar('-'),x*=-1;
	if(x>9) put(x/10);
	putchar(x%10+48);
}
ll lc[V],rc[V],val[V],f[V],pos[V];
ll cnt,opt,v,root;
void update(ll x)
{
	f[x]=f[lc[x]]+f[rc[x]]+1;
}
void split(ll now,ll k,ll &x,ll &y)
{
	if(now==0) x=y=0;
	else
	{
		if(val[now]<=k)
		{
			x=now;
			split(rc[now],k,rc[now],y);
		}
		else 
		{
			y=now;
			split(lc[now],k,x,lc[now]);
		}
		update(now);
	}
}
ll make(ll x,ll y)
{
	if(x==0||y==0) return x+y;
	if(pos[x]<pos[y])
	{
		rc[x]=make(rc[x],y);
		update(x);
		return x;
	}
	else 
	{
		lc[y]=make(x,lc[y]);
		update(y);
		return y;
	}
}
ll getnew(ll x)
{
	f[++cnt]=1;
	val[cnt]=x;
	pos[cnt]=rand()%1000000000+10;
	return cnt;
}
ll find(ll now,ll k)
{
	while(1+1==2)
	{
		if(k<=f[lc[now]]) now=lc[now];
		else if(k==f[lc[now]]+1) return now;
		else k-=(f[lc[now]]+1),now=rc[now];
	}
}
int main()
{
	srand(time(NULL)); //论随机化种子的重要性
	t=in();
	while(t--)
	{
		opt=in(),v=in();
		if(opt==1)
		{
			split(root,v,x,y);
			root=make(make(x,getnew(v)),y);
		}
		else if(opt==2)
		{
			split(root,v,x,z);
			split(x,v-1,x,y);
			y=make(lc[y],rc[y]);
			root=make(make(x,y),z);
		}
		else if(opt==3)
		{
			split(root,v-1,x,y);
			put(f[x]+1);
			putchar(10);
			root=make(x,y);
		}
		else if(opt==4) put(val[find(root,v)]),putchar(10);
		else if(opt==5)
		{
			split(root,v-1,x,y);
			put(val[find(x,f[x])]);
			root=make(x,y);
			putchar(10); 
		}
		else 
		{
			split(root,v,x,y);
			put(val[find(y,1)]);
			root=make(x,y);
			putchar(10); 
		}
	}
	return 0;
}
上一篇:【优化预测】萤火虫算法优化BP神经网络预测【Matlab 1313期】


下一篇:线程队列 concurrent 协程 greenlet gevent