浅谈splay
\(BST\)
二叉查找树,首先它是一颗二叉树,其次它里面每个点都满足以该点左儿子为根的子树里结点的值都小于自己的值,以该点右儿子为根的子树里结点的值都大于自己的值。如果不进行修改,每次查询都是\(O(logn)\)的。
\(Splay\)
\(splay\)是一种支持旋转的二叉查找树,由于旋转可以维护它的深度,使其保持平衡,所以我们又称它为平衡树。一般的平衡树支持的操作它基本都支持,不支持的比如有可持久化。所以我们不多赘述,直接讲它的特性:
旋转
\(zig和zag\)
单次旋转。若一个点是它父亲的左儿子,把它旋转上去,让它父亲成为它的右儿子,则称为\(zig\),否则称为\(zag\)。
根据下面这张图我们可以很好的理解\(zig\)和\(zag\)。
然后这两种单旋操作配合起来一共有四种双旋操作,分别是\(zig-zig\),\(zig-zag\),\(zag-zig\),\(zag-zag\)。
如果你觉得很烦,那你千万不要离开,因为本篇博客,就是来帮助你解忧的。
双旋单旋分情况讨论记不清怎么办?来来来,继续往下看,妈妈绝对不会再担心你的\(splay\)写挂了。
对于一次旋转,只会影响的四个结点,分别是\(x\)本身,\(x\)的父亲\(fa\),\(x\)的儿子\(s\),\(fa\)的父亲\(ffa\)。
设\(t(x)\)为\(x\)与其父亲的关系,左儿子为\(0\),右儿子为\(1\),那么\(t(s)=t(x)xor\) \(1\)。
第一步:把\(fa\)扳下来。
先确定\(fa\)与\(s\)的双方关系,\(fa\)在\(t(x)\)方向上认\(s\)做儿子,\(s\)认\(fa\)做父亲。然后按照此步骤确定\(x\)与\(fa\)的双方关系。
第二步:把\(x\)送上去。
确定\(x\)与\(ffa\)的双方关系。
代码如下:
int t(int u){return son[fa[u]][1]==u;}
void rotate(int u){
int ret=t(u),f=fa[u],s=son[u][ret^1];
int ffa=fa[f],ret_f=t(f);
son[f][ret]=s;if(s)fa[s]=f;//确立s与fa的关系
son[u][ret^1]=f;fa[f]=u;//确立u与fa的关系
fa[u]=ffa;if(ffa)son[ffa][ret_f]=u;//确立u与ffa的关系
}
至此,单旋操作就没了,是不是非常简单呢?
\(splay\)
\(splay\)操作就是让一个点\(x\)一直旋转一直旋转直到它到根为止。有单旋和双旋两种操作,单旋就是直接\(while\)循环调用上面那段代码就行了,双旋有四种情况,但是总的来说也只有两种。
当\(t(x)==t(fa)\)时,那就先单旋\(fa\),否则单旋一次\(x\)。
不管上面那个条件满不满足,都在第二次单旋一次\(x\)。
代码如下:
void splay(int u){
while(fa[u]){//有父亲就不是根
if(fa[fa[u]]){//可以双旋
if(t(fa[u])==t(u))rotate(fa[u]);
else rotate(u);//如上所述
}rotate(u);
}root=u;//旋转到根之后更新根
}
至此,\(splay\)的旋转操作就到此为止了,怎么样,是不是很简单呢?
哦对了,\(splay\)的复杂度十分玄学,在势能分析上来说,应该是\(log\)的,但是保持这个势能需要多多\(splay\),插入要\(splay\),删除也要\(splay\)。
插入
找到一个整个树里面权值最接近插入结点权值的点,然后把要插入的点给它做儿子,然后\(splay\)到根。
删除
把要删除的点\(splay\)到根,然后把它左儿子子树里最大的值旋到根的左儿子处,断开要删除的点的周边所有关系,把右子树的根接在左子树最大值右儿子处即可。
若需区间删除,则把\(l-1splay\)到根,把\(r+1splay\)到根的右儿子处,然后整个右儿子的左子树全部扔掉即可。
大多数时刻区间翻转区间加等操作都是这样,把要处理的区间这样放在根的右儿子的左子树处,然后直接打标记即可,这里我们就不多说了。