【算法】LCT
参考资料
前言
第一次学,感觉这玩意挺抽象……只能写下博客巩固一下印象。
概念
前置知识:树链剖分,Splay
给定一棵树,没有任何的更改操作,询问一些有关树上路径问题(如两点之间的权值和),就可以用树上倍增。而如果在此基础上增添了更改某个点的权值的操作,就需要用到树链剖分了。但是再加一个在保证仍然是一棵树的前提下,断开并连接一些边,就变成了动态树问题了。我们一般用 LCT 来求解动态树问题。
我们可以简单的把 LCT 理解成用一些 Splay 来维护动态的树链剖分,以期实现动态树上的区间操作。对于每条实链,我们建一个 Splay 来维护整个链区间的信息。 ——OI-Wiki
实链
这里出现了一个可能之前没见过的词——实链,它是链剖分的一种形式。为什么 LCT 不用重链剖分或者长链剖分?这是由于在 LCT 里面我们要能自由选择儿子,如果是重链剖分或者长链剖分,它的重儿子是所有儿子里面子树最长的或者深度最大的,没有给我们自由选择的权利。而实链剖分中我们可以自由选择哪些是实儿子,哪些是虚儿子。这种灵活可变性就是我们选择实链剖分的重要原因。
辅助树
辅助树可以理解为多个 splay 组成一个结构,其中每一个 splay 都维护了辅助树中的一条边。鉴于大多数情况下动态树问题可能维护的是一个森林,所以 LCT 实际上维护的是这些辅助树组成的森林。
关于辅助树的主要性质如下:
- 中序遍历每一个 splay 得到的序列对应原树中从上到下遍历的一条路径。也可以理解为中序遍历每一个 splay 得到的序列都是一段深度严格递增的序列。
- 每个节点都在且仅在一个 splay 里面出现
- 实链剖分的实边存在于 splay 里面,虚边是由一个 splay 的根节点指向该 splay 中序遍历最靠前的点在原树中的父亲。虚边和实边的区别在于虚边的儿子认父亲,而父亲不认儿子。
实现
重建重路径:access
函数
它的作用相当于重新建一条从 到根节点的实路径,即在同一棵 splay 里面。这个过程中可能会有一些边由虚变实,相应的也会有一些边由实变虚。
实现的方法是从下到上更新 splay,先将 转到当前这棵 splay 的根,这样方便进行之后的连边操作。这一操作导致需要更换 的实儿子以及更新 的信息。然后把 的父亲按照类似的方法转到它所在的 splay 的根,并将它的实儿子更换为 。
void access(int x){
for(int las=0;x;x=fa[x]){
splay(x);
tree[x].son[1]=las;
pushup(x);
las=x;
}
}
换根:makeroot
函数
在维护路径信息的时候可能会出现路径深度无法严格递增的情况,这时候这条路径就完全不可能在同一个 splay 中,我们就需要将这条路径的一个端点在原树中换到根节点的位置,这样就能保证这条路径一定是严格递增的。
换根可以抽象理解为一条路径上的一些点的深度被更改了。我们先建出要求换成的节点 到根节点的一条实路径,即进行 access
操作。操作后的 一定和根节点在同一条实链上并且深度最深,导致再将 转上来之后它将没有右子树,所以将这棵 splay 翻转后会将它的左子树(即深度比它小的节点)都翻转到右子树(变成深度比它大的节点),这样 没有左子树就变成了深度最小的节点,成为了根节点。
void makeroot(int x){
access(x);
splay(x);
addtag(x);
}
分割:split
函数
相当于把维护 到 路径的 splay 树给拿出来(但不是真的拿,也就是不是真的把边给断了拿出来)。直接把 换为根节点,再建一条从 到 的实路径,把 旋到根,这样它就在 splay 的最右边没有右儿子了。接着就可以很方便地访问 来获取路径信息。
void split(int x,int y){
makeroot(x);
access(y);
splay(y);
}
找根:find
函数
将 和根节点重建出一条实链后,再把 旋上去。然后一直走它的左儿子,因为左儿子的深度会更小,沿路 pushdown
一下,最后按照 splay 的传统把查询到的点给旋上去。
int find(int x){
access(x);
splay(x);
while(tree[x].son[0]){
pushdown(x);
x=tree[x].son[0];
}
splay(x);
return x;
}
连边和断边:link
函数和 cut
函数
对于连边,首先需要判断需要连边的两个点 , 是否在同一棵子树内,在的话显然不合法。然后将 给改成所在树的根,再将 的父亲改为 。
对于断边,先把 改为根节点,然后判断 , 是否有连边(即判断 , 连通,, 的路径上没有其他的链, 没有右儿子),然后改一下 , 的父亲儿子信息。
解释一下判断条件,第一条很显然,第二条可以通过判断 的父亲是不是 , 的右儿子是不是 来解决,第三条需要满足的原因是如果 有左儿子,那么就会存在比 深度小但比 深度大的点,那么 , 之前就不可能有直接的链相连。
void link(int x,int y){
makeroot(x);
if(find(y)==x) return;
fa[x]=y;
}
void cut(int x,int y){
makeroot(x);
if(find(y)!=x||fa[y]!=x||tree[x].son[1]!=y||tree[y].son[0]) return;
fa[y]=tree[x].son[1]=0;
pushup(x);
}
由于 LCT 的每种操作时间复杂度基本基于 access
函数,而它的时间复杂度是 ,那么每种操作的时间复杂度也是 。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)