Splay学习笔记
Splay学习笔记
引入
- 你说的对,FHQ的确是一种极其强悍的平衡树模型,但是\(Splay\)比它码量小,
就是不能分裂合并还不能可持久化,而且大家都在用,根据存在即合理论证法(以及在huge的强制下),我们应该学习\(Splay\)平衡树。
椰树椰汁Splay平衡树,我从小旋到大,品质有保障 (2333
简介
- \(Splay\)树最早由 Daniel Sleator 和 Robert Tarjan
(又是您老)于 1985 年发明.(WHO CARE?),也叫做 伸展树,其核心在于 \(Splay\)伸展操作。 \(^{[1]}\)
[1] 转自OI wiki
什么是伸展操作呢?
- 其实不难理解,就是将指定的一个节点向上 伸,使其变成新根节点,再把它原来的父亲和子树 展 开重新适配,以此维护或构造一个 二叉搜索树(或称二叉排序树)的结构,简称 伸展树 。
伸搜(bush
那怎么伸展捏?
- 最难的部分来力,
坏了cao2333不会
伸展/旋转
下文可能会用到的变量及其含义
- \(child[rt][0/1]\) 表示rt节点的左(0)右(1)儿子;
- \(tree[rt]\) 是结构体数组,存储整个树,包含:
fa表示 rt 节点的父亲;size表示 以该节点为根的子树的 节点数val表示 该节点存储的数值cnt表示 该节点出现的次数(频次)- \(rt\) 表示当前遍历根节点
- \(root\) 表示整颗树的总根
- 对于一个点,我们可以将其 左旋 或 右旋,使其成为新的根节点,这样每次访问的时候就会把这个点给提上来,防止查找次数过多或树退化成链。但是要怎么选择旋转方式 以及按什么顺序非常重要.
- 对此,我们可以 分类讨论 :
关于 旋转
- 旋转是Splay操作的核心。整个伸展Splay实际上就是一组旋转。
- 观察下图

也就是说,用 \(child[rt][0/1]\) 代表 rt 的左/右儿子 :
- 如果此节点是 \(rt=child[fa][0/1]\), 则旋转后节点一定有 \(child[rt][1/0]=fa\).
- 可以将\(|x==y|\)作为条件表达式的真假(0/1),那么
- \(child \Big[rt \Big] \; \Big[\;| child[fa][0]==rt |\;\Big]=fa.\)
看着是不是有点晕?很遗憾,这已经是旋转最简形式了,没有更简单的表示方法。当然,#define 是个好东西,在代码实现时也可以直接用宏定义替换来解决美观度问题,代价是别人可能看不大懂你的代码...qwq
关于 伸展
- 很显然,对于上面那个图,如果我们单独把4旋上去,4就会有整整三个儿子,这显然是不合法的,那么我们应该如何将4的所有属性以及子树变的满足二叉树的性质呢?这就是 \(Splay\)伸展操作.
- 仍然分类讨论,以左旋为例,还是这个图,发现:

- 衡好,右旋大家可以自己推一推,就是把上面右边的树转回左边的树。
- 我们由此又得出了一组简洁的柿子:
- 在左旋时:\(child[fa][1]=child[rt][0]\)
- 在右旋时:\(child[fa][0]=child[rt][1]\)
- 这时候就有一些喜欢压行的
劳什子神犇们要说了:我不想写 if else,有没有和上面一样的整式子? - 答案当然是————
你压行干嘛有的兄弟,有的,就是比上面那个还丑- \(child \Big[fa \Big] \Big[|\;child[fa][1]==rt\;| \Big]=child \Big[ rt\Big]\Big[|\;child[fa][0]==rt\;|\Big].\)
让大家感受一下这个式子有多臭————它的\(\KaTeX\)代码:
$child \Big[fa \Big] \Big[|\;child[fa][1]==rt\;| \Big]=child \Big[ rt\Big]\Big[|\;child[fa][0]==rt\;|\Big].$- 它的实现也很shit,就,大家可以选择压行还是正常写(虽然压行码量小就是了)
附赠一个忘记从哪里看到的口诀:右子左旋父变左,左子右旋父变右;左旋原来左子右,右旋同理托给爹
#include<bits/stdc++.h>
using namespace std;
void rotate(int rt){
int fa=tree[rt].fa; //亲跌Father
int gdfa=tree[fa].fa; //祖父GrandFather;
bool k=(child[fa][1]==rt);//此处判定rt是否为右子树,1真0假,可以推出1左旋0右旋。
//更改祖父节点与本节点的关系,一定要记住是双向的(不然另一边不认亲的)
child[gdfa][child[gdfa][1]==fa]=rt;
tree[rt].fa=gdfa;
//把自己的某个子节点换到父节点上(托孤(bush)
child[fa][k]=child[rt][!k];
tree[child[rt][k]].fa=fa;
//直接把本节点变成父节点,右节点左旋(1)左子(0)为父,左节点右旋(0)右子(1)为父
child[rt][!k]=fa;
tree[fa].fa=rt;
}
操作过程
- 由于篇幅过长,
而且我也没看懂,为什么要做这么操作 的证明只有我们学长会证 - 而且cao2333这个蒟蒻实在太弱了,至今也不敢保证我的思路没问题,在这里仅仅提供我自己的理解
- 大部分的图都是从学长和 OI wiki 那里顺的,如有冒犯还请见谅
咕咕咕(我是分鸽符)
- 首先,我们需要注意到:如果我们把一个节点旋到指定的一个地方,也就是让他 取代某个父节点(这个父节点称为目标),其实在最后不好判断有没有旋完.(如果你旋到那个目标了,那个目标就变成了儿子,那这时候是旋完了还是没完呢?)
- 我们可以借鉴一下隔壁物理组的思想
(其实并不隔壁),选一个 相对静止 的节点作为参照系。这里最方便的就是目标的跌(节点自己的某个爷爷,下称 目标根 )————当节点旋到成为他爷爷(目标根)的直接儿子时,我们认为他已经旋到原来目标的位置了(您家辈分着实有点乱啊)
于是我们有三种不同情况:
zig
- 当自己的 爷爷 就是目标根时,直接和自己的跌(目标)旋一下

zig-zig
- 当自己的爷爷不是目标根,而且 自己和爹的关系 与 爹和爷爷的关系 相同时(一般称三者在一条直链上),可以让父节点先和目标根旋一下(顺便把自己拉上去),再把自己旋上去。

注意:此图中的节点(尤其是子节点)不一定是纯粹的单个节点,可能是某个子树的集合,因此请勿产生 “我这么旋不是更方便吗” 的
跟我一样的叛逆想法,若是在不理解可以试一试手模一下,看一看会不会使树平衡,手模更方便理解
- 如果你试过直接一步一步旋,你可能会发现 树中途拧成了一个麻花 ,这会使树更加复杂,应该避免。
zig-zag
- 如果节点向上遇到这种"麻花"型,我们可以直接一步一步跳,顺便也把麻花捋成正常的了

- 注意,这里旋转的目标已经和上一种完全不一样了,不能并论
- 这就是所有的旋转操作。此时就又有
懒惰聪明的小朋友要问了:- 啊蒟蒻蒟蒻,你这样好麻烦啊,我还得照顾他的爹和爷爷,难道只能写 if else吗?
其实就是你又双叒叕想压行了吧- 实则不然。对于这三种
恶心人讨人喜的情况,我们用一种同样讨人喜的方式写就可以,但我还是推荐大家直接写if else版本,因为压行实在是太shit了!!!
#include<bits/stdc++.h>
using namespace std;
void Splay(int rt, int to){
int fa=tree[rt].fa;
int gdfa=tree[fa].fa; //同上
int k=!( (child[gdfa][0]==fa)^(child[fa][0]==rt) );//判断是否在一条链上
//这里k的取值回顾^异或的用法:同0异1
if(gdfa==to) rotate(rt); //Zig,直接旋
else if(k){ //Zig-Zig,
rotate(fa); //先旋父节点
rotate(rt); //再把自己旋上去
}
else{ //Zig-Zag
rotate(rt); //直接一步一步旋
rotate(rt);
}
//压行版本,请 不要 与上面那种连用
if (gdfa!=to) k ? Rotate(fa) : Rotate(rt);
rotate(x);
}
本段鸣谢
zig&zag趣谈
在一家大公司里(一棵树上),一些职员节点不满于老板(根节点)的暴政以及不平衡行为,打算取代老板(成为新的根),重振平衡树荣光。
但是公司有一些奇怪的规定:
1.一个节点最多有两个直接下属(儿子),要求其左下属(左儿子)的能力(值)比他低,右下属(右儿子)能力比他高;(二叉树性质)
2.在一个节点受到提拔时,他的所有下属节点也会一并提拔,相当于提拔了整个部门;(同理)
3.想要成为老板,就必须靠升职使得自己的地位比老板还高; (使根节点变成自己的子节点)
4.[神犇](毒瘤测试点)在访问某个节点时,必须按照公司规定,从老板一级一级向下寻找节点 (二叉搜索树顺序)
5.老板实际上是某个[空节点](节点0)的下属,他们之间没有任何左右关系,或者说[空节点]的左右下属都是老板。
这无疑是为了激发公司活力。节点们十分高兴,都想最早被[神犇]访问,于是开始了他们的平衡之路——
一个卑微的A结点(任意节点)也有他的想法,他曾经被[神犇]专门访问过(被查询/修改过),为了防止[神犇]的下一次到访因为路太远(树退化成链)而脸色发黑(TLE),于是他决定也要超越自我,成为新老板。
首先,公司里的一些员工和他们的下属关系十分相似,有一些左下属形成了左下属的链,链上的节点都是其父节点的左下属;右下属们也同理。(Zig-Zig类型)A结点发现这种情况下,链上节点们因为平时上下级关系相似好处理,都很好说话:只要你和你的上司聊一聊,他就会考虑晋升自己并且根据2.把你也提拔上去。显然这种只需要把问题都甩给上司的方法对于A结点要省事得多。于是A结点凭借高超的卖萌技艺(awa)成功晋升到链的顶端.
但是并不是所有节点都能和自己的上下级打好关系。有些节点处于一种尴尬的境地:他是上司的左下属,但是A结点是他的右下属(Zig-Zag类型)。这种情况节点由于不喜欢和自己属性不一样的节点会拒绝A节点的请求(其实就是不好旋)。但是A结点是有一定实力的,他可以直接用强硬手段把自己晋升为上司,直接自己处理新上司之间的关系。
凭借这种方法,A结点已经晋升到了老板的下属。自己想要变成[空节点]的下属,但是[空节点]没有左右下属,他的老套路失效了(Zig类型)。
聪明的A节点发现这时候完全可以不管[空节点],反正他坐那里不动,直接把老板踹下去就行了(直接旋)
于是A结点成为了新的老板,同时有一些新的节点也要进入公司(新加入节点),由于A节点实在太累了就让新节点自己找位置,但是这可能会使树变得不平衡,此时就会有新的B节点要来取代A节点变成老板(其他平衡splay操作),但那显然不是A节点要操心的事了,A现在只想睡觉得到AC 100pts!!
- Emm,其实你已经学完Splay了 O_o
- 真的,\(Splay\)一点都不难,码量还小
并非,只是... 特性和它推式子一样臭
咕咕咕
- 其他的操作怎么写?其实几乎和FHQ一模一样,直接把查询节点旋到根节点直接查就行
- 咕咕咕,完整代码就不写了,网上的Splay真是一个人一个写法,根本就统一不了,所以此处省略以免误导读者大大
时间复杂度
- \(O((n+m)log\;n)\)
- 证明我不会,
- 就...你还是看 \(OI\;wiki\) 叭
注意事项
- \(Splay\)是\(LCT\) (\(Link \; Cut \; Tree\))的基础,个人认为在非必要条件下还是 无旋Treap-FHQ 更通用一些
- \(Splay\)不支持分裂合并操作,不支持可持久化,且时间复杂度常数远大于FHQ.
- \(Splay\)码量小,出错好找,大部分Coder都应该学习-------huge
例题
咕
\(\color{red} 彩 \color{blue}蛋\)
论我为什么不把这几篇文章\((treap,Splay,FHQ)\)归到一个文章里,因为这几个全都太难了!
cao2333学不会啊(雾
无奖竞猜:cao2333在这篇文章里放了几只"咕"?

浙公网安备 33010602011771号