今天学了个叫 “LCT” 的东西,很多人估计越听越懵。
Link-Cut Tree 确实是个很不好理解的东西,就连我也是。
就算是老师讲也感觉很模糊。
首先,这个 LCT 也可以理解为“用 Splay 来维护树剖”,这里的树剖指的是实链剖分(专门为 LCT 服务的剖分方式)。
一棵树中,处于同一条实链的节点用同一个 Splay 维护。由于 Splay 灵活多变,完全可以代替线段树。
然后,每一个 Splay 内维护的节点键值是他们在原来树中的深度。即对于一个 Splay 节点键值 x x x,左子树的所有节点在树中的深度都小于 x x x,右子树的所有节点在树中的深度都大于 x x x。
于是,代码的前半部分完全用来写 Splay。
注意,有一个重点就是,树中的实链(即一棵 Splay)中的节点连的是双向边,而非实边连的是单向边(儿子认父,父不认儿子),这样有助于判断一个节点是否是 Splay 的根。
但是,之后呢?我们定义一个函数 a c c e s s ( x ) access(x) access(x) 表示,现在把 r o o t , . . . , x root,...,x root,...,x 这一条链搞成一条实链,并且 x x x 下面不接任何实边(注意 r o o t , . . . , x root,...,x root,...,x 这些节点原本接的实边要删除)
如上图,红色的为实边(有些节点也可以不接实边,比如 7 7 7)。
接下来执行 a c c e s s ( 5 ) access(5) access(5)。
执行 a c c e s s ( 8 ) access(8) access(8)。
可以发现,每做一次 a c c e s s ( x ) access(x) access(x),其实就是把 r o o t , . . . , x root,...,x root,...,x 重构成一条实链,并单独用一个 Splay。
a c c e s s access access 函数是 LCT 中最重要的部分,也是 LCT 中唯一连接实线的方式。
void access(int x) //x 不为空,直到根为止
{
int y=0;
while(x)
{
splay(x);
a[x].c[1]=y; //节点 x 原来接的实线清空,重新接到下面的节点,原来接的节点必然深度更深,即在平衡树右边
a[y].fa=x;
pushup(x);
y=x;
x=a[x].fa; //下一次更新实线用到
}
}
函数 e v e r t ( x ) evert(x) evert(x) :把 x x x 作为 x x x 所在树的与根。
考虑用 a c c e s s ( x ) access(x) access(x) 把 x x x 和 r o o t root root 连接,然后伸展到平衡树的根,最后利用翻转来保证所在平衡树的深度。
比如执行 e v e r t ( 8 ) evert(8) evert(8),连接了 1 , 7 , 8 1,7,8 1,7,8,以深度为键值建出平衡树:
把 8 8 8 节点伸展到根:
你会发现,如果把 8 8 8 作为根,有且只有这棵平衡树的深度需要重构,只需要对整棵树进行翻转即可。
翻转必然要打标记,必然要下放标记。因此在每次 Splay 伸展时,必须先从根传标记。
void evert(int x)
{
access(x);
splay(x);
a[x].rev^=1; //翻转标记
}
函数 f i n d r t ( x ) findrt(x) findrt(x) 查找 x x x 所在的树的根。
显然,先用 a c c e s s access access 连接 r o o t root root 和 x x x,然后在平衡树中找深度最小的,即一直往左走。
int findrt(int x)
{
access(x); //连接 root,x
splay(x); //把 x 伸展到平衡树的根,也可以理解为找平衡树的根的一种方式
while(a[x].c[0]) //一直往左走
{
x=a[x].c[0];
}
return x;
}
函数 l i n k ( x , y ) link(x,y) link(x,y),连接原来树上 ( x , y ) (x,y) (x,y) 两个节点。
类似于并查集的合并,我们可以先把 x x x 作为树根