前置知识
- Splay基本操作
- 树链剖分
- Splay维护区间信息
概念
链剖分
链剖分,是指一类对树的边进行轻重划分的操作,这样做的目的是为了整体地维护树上的连续一段节点/边,以优化时间复杂度。链剖分一般分为三种,重链剖分,实链剖分和并不常见的长链剖分。
重链剖分
重链剖分其实就是我们所说的树剖。
定义每个点的size域为它的儿子的个数,则定义某个点的重儿子为它的所有儿子中,size域最大的那个儿子。则连接它的它重儿子的边为重边,而连接其他儿子的边称为轻边。
若干重边连接在一起构成重链,因为重链上的节点编号连续,因此可以用某些数据结构维护重链内部的关系。
实链剖分
与重链剖分不同地,实链剖分并不按照size域来决定那一条边是重链,而是可以变化的,因此一般使用更加灵活的Splay来维护(据说还可以用Treap维护),而LCT就是以实链剖分为基础的。
长链剖分
长链剖分在信息学竞赛中并不常见。它定义所有节点的深度为其到根的距离,则某个节点的重儿子为它的所有子树中,含有最大深度的节点的子树,其余定义和重链剖分基本一致。
LCT
在实链剖分的基础上,LCT支持很多重链剖分做不到的操作
- 查询、修改树上信息
- 换根
- 动态连边、删边
- 合并、分离不同的树
- 维护联通性(适用范围比并查集更广)
主要性质/定义
- 每个Splay维护的是一条在原树中深度严格递增的路径,且以深度为关键字排序。
图中的每个方框表示一棵Splay,由于Splay的性质,旋转不会破坏中序遍历,因此不论怎么旋转都可以保证树的形态唯一。 - 每个节点包含且仅包含于一个Splay中
- 树链剖分中,虽然说是把每条链交给一个线段树管理,但更多时候却是重新编号,在保证重链的编号连续的前提下,用一大棵线段树来管理。
类似地,LCT中,所有的Splay其实是一棵Splay,其中的实线边为真正的Splay的边,最多只有两条(即左右儿子),而且关系是双向的,而虚边则仅仅由一个一棵Splay指向另一棵Splay中的某个点,更准确来说,应该指向另一棵Splay的根,关系是单向的,也就是说,父亲是不记录它的编号的。
为什么这里可以直接指向根呢?因为根据Splay的性质,可以很快地找到真正指向的元素—,即Splay中的最小元素。
操作
作用:把
到树根的唯一路径上的所有边变为实边,且让
成为所在的重链中的深度最大的结点。
下面以
为例子。
具体怎么实现呢?根据性质一,旋转不会改变原树的结构,因此可以先把
提到它所在的Spaly的根,即
。
因为要保证
之后,
是其所在的重链中深度最深的结点,因此不可能有结点的深度比
还大,所以必须把
的右子树独立开成另外一棵Splay。
因为虚边是单向的(父节点不认子节点),因此在实现上直接让
的右儿子为空就好啦。
对应到原图即为:
接着
然后关键部分来了。既然我们要让
到树根的唯一路径上的所有边变为实边,那么
自然也不例外。因此,我们要让
的边成为实边,那就让
取缔
的右儿子,为什么一定是右儿子呢?
回顾一下性质1:
每个Splay维护的是一条在原树中深度严格递增的路径,且以深度为关键字排序。
也就是说,在同一棵Splay中必须按照深度递增,而N是I的儿子,因此N的深度比较N更大,所以必须取缔右儿子的位置。
对应到原图:
然后再
同理,
取缔
的右儿子。
对应到原图
取缔
的右儿子。
对应到原图:
大功告成!
作用:让
成为树根。
很多时候,仅仅让一个点和根节点在同一棵Splay中并不能满足要求,因为可能遇到某些节点的深度是相同的,根据性质一,这两个点不可能再同一棵Splay中,这种时候,就必须换根。
下面以
为例子
先
。
然后
。
这里不论是单旋还是双旋都没有问题,不会影响程序的正确性(因为不会影响到根节点和其他节点的大小),但有可能对时间有一定影响。
然后就是重头戏啦!把
所在的整棵 Splay 对称翻转。即让深度小的变为深度大的,深度大的变为深度小的。
但是这个操作的时间复杂度似乎是
的,那有没有什么好方法呢?回忆一下曾经的Splay模板之一:文艺平衡树
因此可以打上旋转标记。
在原图也就是把整条链从
拎起来。
完成!
作用:让
和
在同一棵Splay中。
有了access和rooted函数,就可以方便地拉出一条链了。