【学习笔记】动态树 Link-Cut Tree

1|0- 闲话

LCT 优秀博客:

2|1- 前置知识

  • 必学Splay
  • 重要树链剖分 / 重链剖分(虽然并不需要用到,但是了解重链剖分的思想还是有用的)。
  • 必学」实链剖分。

实链剖分是一种动态的剖分方式。

对于一个点连向它儿子的所有边,我们选择一条边进行剖分。
被选择的边称为实边,其他边为虚边。
实边连接的儿子称为实儿子。
对于一条实边连接成的链,称为实链。

实链剖分的剖分结果是可变的,可以灵活调整。

重链剖分的区别就是,重链剖分需要找到儿子子树大小最大的一个连重边,而实链剖分不需要。

给定一棵树,有以下操作:

  • 修改 x 的点权。
  • 求出 x,y 的简单路径的点权和。
  • 修改 x 子树每一点的点权。
  • 求出 x 子树的点权和。

一个很简单的「树链剖分」题目,不是吗?

如果我们增加几个操作呢?

  • 断开 xy 这一条边。
  • 连上 xy 这一条边。
  • 把这棵树改成以 x 为根。

很显然,因为这棵树要动态删边和加边,且有换根操作,维护静态树的树链剖分就无法处理这类题目了,「动态树 Link-Cut Tree,LCT」应运而生。

具体的,LCT 可以维护以下操作(引自 FlashHu cnblogs):

  • 查询、修改链上的信息(最值,总和等)
  • 随意指定原树的根(即换根)
  • 动态连边、删边
  • 合并两棵树、分离一棵树
  • 动态维护连通性
  • 更多意想不到的操作

因为 LCT 是动态的数据结构,所以线段树等已不适合维护,引入「Splay」这种平衡树来维护之 [1]

LCT 实质上维护了一个森林,每棵树 都由若干棵 Splay 维护。有如下性质:

  1. 每棵 Splay 都维护一条原树的路径,这条路径满足节点深度依次增大,且中序遍历 Splay 得到的每个点的深度序列严格递增。单独的一个点也可以是一棵 Splay。

    • 举个例子,这棵树的构造为 1(2,3),即 1 号节点为树根,深度为 12,3 号节点分别为它的左右儿子,深度为 2。那么这个 Splay 森林可以是这样的:
    1. [1,2][3],第一棵 Splay 维护 12 这条路径,深度单调递增(1,2),第二棵维护 3,单独的一个点。
    2. [1,3][2],第一棵 Splay 维护 13 这条路径,深度单调递增(1,2),第二棵维护 2,单独的一个点。
    3. [1][2][3],三个点都为一棵独立的 Splay。

    注意 [1,2,3] 这棵 Splay 是不合法的,因为 2,3 的深度相等。

  2. 每个节点被包含且仅被包含在一棵Splay 内。

  3. 由以上两条性质我们可以得出,每个节点只能和它的儿子连一条实边,其余的儿子都和他连虚边,并且每一条虚边的儿子所在的 Splay 要指向这个节点。但是这个节点并不能指向其儿子的 Splay(即 FlashHu 博客中的认父不认子)。

3|0- 具体操作

3|1- access(x)

  • LCT 最核心的操作。

  • 打通根节点和指定节点的路径,即把根节点和 x 中间的路径都变成 实边,形成一条以根节点开始,指定节点结束的 Splay。

来几张图 [2]

假设一开始实边和虚边是这么划分的:

那么所形成的 Splay 森林可能是这样的(绿框中为一个 Splay):

现在我们要 access(N),把 AN 的路径都打通成实边,变成一颗 Splay。

根据性质 3,原来有些实边要变虚(因为 AN 的有些虚边要变实,同层只能有一条实边连向父亲)。那么原树可能要变成这样:

我们一步一步自底向上拉。

首先 splay(N),把 N 转到所在 Splay 的根。

因为要 以指定节点结束,所以比他深且在一颗 Splay 中的点要去除。

因为性质 1,中序遍历 Splay 得到的每个点的深度序列严格递增,所以我们把 N 右边的点去掉即可。即把 NO 这条边变虚。直接把 N 的右子树变空,然后让 O 所在 Splay 指向 N 即可。

如下图:

接下来要打通 IL 的边,首先找到 N 所在 Splay 指向的节点 I,并 splay(I),让 I 转到其所在 Splay 的树根,这样保证它的右儿子肯定是它在原树中连的虚边(性质 1),把它的右子树置空。

然后就可以连接 IL 了,因为 L 所指向的点是 I,把 N 直接连到 I 的右子树即可。

I 指向 H,接着 splay(H),把 H 的右子树直接置为 I 即可。

H 指向 A,于是 splay(A),把 A 的右子树更新成 H

于是 AN 就在一个 Splay 里了,且正好中序遍历以 A 开始,以 N 结束。

代码很简单,只需要四步:

  1. splay 当前节点,转到根。
  2. 找到它所指的父亲,换右儿子。
  3. 更新信息,pushup
  4. 把当前节点变成它的轻边所指的父亲,转 1
inline void access(int x){ for(int y=0;x;y=x,x=fa[x]) splay(x),ch[x][1]=y,pushup(x); }

3|2- makeroot(x)

  • x 拉到整棵树的根。

在介绍 makeroot 前,先来回顾一下 Splay 的区间反转操作,不熟悉的可以看一下模板题

1|0- pushr(x)

我们注意到,将 [l,r] 这一段区间反转,相当于对于 lidr 的每一个节点的左右子树自上而下 反转。

引用几张图 [3]

这是一棵树,那么如果我们想翻转 [2,4] 这个区间,只需要 splay 15,使得 5 的左子树都是 >1<5 的(二叉平衡树的性质),于是只需要反转 [2,4] 的左右子树了。

但在这个地方我们可以考虑打个标记,标记的存在就只在于记录现在对于当前节点应不应该翻转两个子树。

接下来回到 makeroot

首先显然要把根节点到 x 的路径打通,否则根节点和 x 都不在一棵 Splay 中,谈何换根。

所以我们先 access(x),然后根节点到 x 就是一条实路径,且中序遍历以根节点开头,以 x 结尾。不难发现此时 x 就是这颗 Splay 中深度最深的点。然后我们先 splay(x),使得 x 节点为这颗 Splay 的根,(注意不是整棵树的根,因为先序遍历仍以原先根节点开始)。这时候 x 没有右子树。因为 x 的深度最深,这时候我们翻转一下,pushr,把这一棵 Splay 深度都改变,这时候 x 就变成的这棵树最上面的节点(真正的根,它没有左子树),大功告成。

inline void pushr(int x){ swap(lc(x),rc(x)); r[x]^=1; } inline void makeroot(int x){ access(x); splay(x); pushr(x); }

3|3- findroot(x)

  • 找到 x 所在原树的根,主要用来判断两点的连通性(即如果 findroot(x)=findroot(y) 表明 x,y 在一棵树中)。

我们先把根节点到 x 的路径打通,然后 splay(x),把 x 转到这棵 Splay的根(不是原树的根,没有破坏结构),这时候根据二叉排序树的性质,所有深度比 x 小的点都在 x 的左子树,循环找下去,知道叶节点即可。

注意往下找左儿子的时候,一定要下放翻转标记,不然可能会导致 Splay 信息不正确

inline int findroot(int x){ access(x); splay(x); while(lc(x)) pushdown(x),x=lc(x); // 一定要 pushdown! splay(x); // 保证复杂度 return x; }

3|4- split(x,y)

  • 指定出一条 xy 路径的 Splay。

新的 makeroot 已经出现,怎么能够停滞不前?

makeroot(x),把 x 变成当前树的根,然后 access(y),提取 xy 的路径。最后 splay(y) 保证复杂度。这样访问这个 Splay 的时候只需要访问 y 就可以了。

inline void split(int x){ makeroot(x); access(y); splay(y); }

3|5- isroot(x)

  • 判断当前节点是否是它所在 Splay 的根。

原理很简单,如果他是 Splay 的根(即它和它的父亲连的是虚边),它的父亲的儿子里没有它(它的父亲连到它的实儿子了)。

inline bool isroot(int x){ return lc(fa[x])==x || rc(fa[x])==x; }

3|6- link(x,y)

  • 连上 xy 的边。

可以自行决定把 x 的父亲设为 y 还是把 y 的父亲设为 x,这里我把 x 的父亲设为 y,即在 x,y 间连一条轻边。

代码也很简单,如下:

inline bool link(int x,int y){ makeroot(x); // 使 x 成为它所在的树的根 if(findroot(y)==x) return 0; // 两点已在一棵树内,连边不合法 fa[x]=y; return 1; }

如果题目保证连边合法,代码就可以更简单:

inline void link(int x,int y){ makeroot(x); fa[x]=y; }

如果

4|0- Reference

[1]:因为 LCT 的 makeroot 等操作需要翻转一棵树,使得 Treap 等平衡树均已不适用,但是 FHQ Treap 或许也可以维护,详见 https://immortalco.blog.uoj.ac/blog/2342

[2]:引自 https://www.cnblogs.com/flashhu/p/8324551.html

[3]:引自 https://www.luogu.com.cn/blog/pks-LOVING/splay-chu-li-ou-jian-cao-zuo-fan-zhuai-cao-zuo-reverse


__EOF__

本文作者TheSky233
本文链接https://www.cnblogs.com/TheSky233/p/17034263.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   TheSky233  阅读(67)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示