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实际上就是一组旋转。
  • 观察下图
  • Ask Me for Help

也就是说,\(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

  • 如果节点向上遇到这种"麻花"型,我们可以直接一步一步跳,顺便也把麻花捋成正常的了
  • 图*3
  • 注意,这里旋转的目标已经和上一种完全不一样了,不能并论

  • 这就是所有的旋转操作。此时就又有 懒惰 聪明的小朋友要问了:
    • 啊蒟蒻蒟蒻,你这样好麻烦啊,我还得照顾他的爹和爷爷,难道只能写 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在这篇文章里放了几只"咕"?

posted @ 2026-07-15 14:49  听凭风引adj  阅读(9)  评论(0)    收藏  举报