前言
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