SizeBalancedTrees

前言

BST一直都是卡常的领域,为了不被卡常,我们需要一个更平衡的BST,SizeBalanceTree就是不错的选择。

旋转

众所周知,平衡树分为两类,非旋平衡树和旋转平衡树,
SizeBalanceTree是依靠旋转来保持平衡的。

左旋

插个图片大家好理解
左旋的意思就是把当前节点移到左边去,同时保持树的中序遍历不变,右旋也是同理(代码与左旋对称),注意保持树的中序遍历不变是非常重要的,它决定了树的搜索性质是否变化;

右旋

代码
inline void Rotate(int &now,bool flag)
{
	int son=Son[now][!flag];
	Son[now][!flag]=Son[son][flag];
	Son[son][flag]=now;
	Update(now);Update(son);
	now=son;
}

维护

有人说SizeBalanceTree是AVL-Tree的变体,但我不觉得全是,简化版SizeBalanceTree的Maintain维护代码量非常小,也非常容易记忆,详情见代码;

代码
inline void Maintain(int &now,bool flag)//左右情况一样,假设这里是左,即flag=false 
{
	if(Size[Son[Son[now][flag]][flag]]>Size[Son[now][!flag]])
	//左儿子的左儿子比我的右儿子还大,我带着左儿子的右儿子去我的右儿子家平衡一下势力 
		Rotate(now,!flag);
	else if(Size[Son[Son[now][flag]][!flag]]>Size[Son[now][!flag]])
	// 左儿子的右儿子比我的右儿子还大,先让他自己平衡一下,再把他的右儿子带过来这边 
		Rotate(Son[now][flag],flag),Rotate(now,!flag);
	else return;//一家和睦 不再调整位置 
	Maintain(Son[now][0],false);//还不平衡?全部都重新调整一下 
	Maintain(Son[now][1],true);
	Maintain(now,false);
	Maintain(now,true);
}

插入

SizeBalanceTree的插入和普通BST插入是一样的,维护节点左小右大的性质

代码
inline void Insert(int &now,Type data)
{
	if(!now)
	{
		now=top>0?Trash[top--]:++len; 
		Data[now]=data;Size[now]=1;
		Son[now][1]=Son[now][0]=0;return;
		//这里回收了一下废弃节点,Trash[]是一个栈
	}
	Size[now]++;
	if(data<Data[now])Insert(Son[now][0],data);//左小
	else			  Insert(Son[now][1],data);//右大
	Maintain(now,data>=Data[now]);
}
关于Splay的Cnt[]数组

为什么不把同一个data存在一个节点里呢,因为SizeBalanceTree依赖Size[]数组实现平衡,所以如果Size[]加上了同data的个数,Maintain了之后并不平衡,也就是说高度和原来没有区别,看各位有没有其他方法避免了(因为后面的操作可能会因此出Bug);

删除

删除视情况而定,如果是精准删除可能要整两个log2n的时间复杂度,超级简化版的删除是这样的:

代码1
inline int Delete(int &x,Type data)
{
	Size[x]--;Type tmp;
	if(Data[x]==data||(!Son[x][0]&&Data[x]>data||(!Son[x][1]&&Data[x]<data)))
	{
		tmp=Data[x];
		if(!Son[x][0]||!Son[x][1])x=Son[x][0]+Son[x][1];
		else Data[x]=Delete(Son[x][0],data+1);
		return tmp;
	}
	if(data<Data[x])tmp=Delete(Son[x][0],data);
	else			tmp=Delete(Son[x][1],data);
	return tmp;
}
优点

这个代码的优点就是快,解释一下代码第八行Delete(Son[x][0],data+1)的意思,因为二叉搜索树的性质,左子树的值都小于当前节点,那也就是说Delete返回的肯定是x的前驱,这样就可以用前驱交换当前节点的值了;

缺点

这种做法没有办法防止误删节点(不过一般没有这么毒瘤的题,读者自行选择),这时候你们肯定就要问了,为什么不加判断?因为本来是在不确定的基础上找前驱,如果你判了就肯定Re了;

想要解决只能暴力找前驱,大量操作下效率肯定不如代码1:

代码2
inline void Delete(int &now,Type data)
{
	if(data==Data[now])
	{
		if(!Son[now][1]||!Son[now][0]){Trash[++top]=now;now=Son[now][1]+Son[now][0];return;}
		//这里是回收节点和左右子树覆盖当前节点的操作
		int tmp=Son[now][1];while(Son[tmp][0])tmp=Son[tmp][0];
		//暴力搜索前驱
		Size[now]--;Delete(Son[now][1],Data[now]=Data[tmp]);return;
	}
	if(Size[now]<=1)return;//找不到该节点
	if(data<Data[now])Delete(Son[now][0],data);
	else			  Delete(Son[now][1],data);
	Update(now);//不知道删了还是没删,直接更新
}
为什么删除后不用维护

有多少删除就有多少插入,插入频率和删除频率大致相等时,插入可以维护删除带来的不平衡,且删除不会使高度增加;折中一下,效率反而比你时刻维护还要高(反正删除频率比插入少一大截)

剩下的平衡树基本操作

接下来的操作比较简单,详请看代码中的注释

GetRank代码
inline int GetRank(int now,Type data)
{
	if(!now)return 1;//为什么是1?定义第一个人的排名是1
	else if(data<=Data[now])return GetRank(Son[now][0],data);
	//data<=Data[now] 排名等于data在左子树的排名
	else return 1+Size[Son[now][0]]+GetRank(Son[now][1],data);
	//data>Data[now] 排名等于左子树的大小+当前节点+右子树的排名
}
Select代码
inline Type Select(int now,int rank)
{
	if(Size[Son[now][0]]+1==rank)return Data[now];
	//如果rank是当前值的排名 那返回值就是当前值
	else if(rank<=Size[Son[now][0]])return Select(Son[now][0],rank);
	//去左边找
	else return Select(Son[now][1],rank-1-Size[Son[now][0]]);
	//右边的排名还要减去(左子树的大小+当前节点)
}
Prev代码(前驱)
inline Type Prev(int now,Type data)
{
	if(!now)return data;
	//data防错,如果return data;就是没有前驱,后继同理
	else if(data<=Data[now])return Prev(Son[now][0],data);
	else{int tmp=Prev(Son[now][1],data);return tmp==data?Data[now]:tmp;}
	//data大于自己时,自己有可能是前驱,所以特判,后继同理
}
Succ代码(后继)
inline Type Succ(int now,Type data)
{
	if(!now)return data;
	else if(data>=Data[now])return Succ(Son[now][1],data);
	else{int tmp=Succ(Son[now][0],data);return tmp==data?Data[now]:tmp;}
}

找后继和找前驱的代码完全对称,有人和我讲:不懂Prev和Succ是什么意思?于是我加了这两个括号

后记

平衡树这种算法,一定要平时多实现,这样才能更熟练,谢谢大家的观看;

完整代码,有需自取:https://www.luogu.com.cn/record/36959522

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值