【算法】LCT

参考资料

前言

第一次学,感觉这玩意挺抽象……只能写下博客巩固一下印象。

概念

前置知识:树链剖分,Splay

给定一棵树,没有任何的更改操作,询问一些有关树上路径问题(如两点之间的权值和),就可以用树上倍增。而如果在此基础上增添了更改某个点的权值的操作,就需要用到树链剖分了。但是再加一个在保证仍然是一棵树的前提下,断开并连接一些边,就变成了动态树问题了。我们一般用 LCT 来求解动态树问题。

我们可以简单的把 LCT 理解成用一些 Splay 来维护动态的树链剖分,以期实现动态树上的区间操作。对于每条实链,我们建一个 Splay 来维护整个链区间的信息。 ——OI-Wiki

实链

这里出现了一个可能之前没见过的词——实链,它是链剖分的一种形式。为什么 LCT 不用重链剖分或者长链剖分?这是由于在 LCT 里面我们要能自由选择儿子,如果是重链剖分或者长链剖分,它的重儿子是所有儿子里面子树最长的或者深度最大的,没有给我们自由选择的权利。而实链剖分中我们可以自由选择哪些是实儿子,哪些是虚儿子。这种灵活可变性就是我们选择实链剖分的重要原因。

辅助树

辅助树可以理解为多个 splay 组成一个结构,其中每一个 splay 都维护了辅助树中的一条边。鉴于大多数情况下动态树问题可能维护的是一个森林,所以 LCT 实际上维护的是这些辅助树组成的森林。

关于辅助树的主要性质如下:

  1. 中序遍历每一个 splay 得到的序列对应原树中从上到下遍历的一条路径。也可以理解为中序遍历每一个 splay 得到的序列都是一段深度严格递增的序列。
  2. 每个节点都在且仅在一个 splay 里面出现
  3. 实链剖分的实边存在于 splay 里面,虚边是由一个 splay 的根节点指向该 splay 中序遍历最靠前的点在原树中的父亲。虚边和实边的区别在于虚边的儿子认父亲,而父亲不认儿子。

实现

重建重路径:access 函数

它的作用相当于重新建一条从 x 到根节点的实路径,即在同一棵 splay 里面。这个过程中可能会有一些边由虚变实,相应的也会有一些边由实变虚。

实现的方法是从下到上更新 splay,先将 x 转到当前这棵 splay 的根,这样方便进行之后的连边操作。这一操作导致需要更换 x 的实儿子以及更新 x 的信息。然后把 x 的父亲按照类似的方法转到它所在的 splay 的根,并将它的实儿子更换为 x

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 中,我们就需要将这条路径的一个端点在原树中换到根节点的位置,这样就能保证这条路径一定是严格递增的。

换根可以抽象理解为一条路径上的一些点的深度被更改了。我们先建出要求换成的节点 x 到根节点的一条实路径,即进行 access 操作。操作后的 x 一定和根节点在同一条实链上并且深度最深,导致再将 x 转上来之后它将没有右子树,所以将这棵 splay 翻转后会将它的左子树(即深度比它小的节点)都翻转到右子树(变成深度比它大的节点),这样 x 没有左子树就变成了深度最小的节点,成为了根节点。

void makeroot(int x){
    access(x);
    splay(x);
    addtag(x);
}

分割:split 函数

相当于把维护 xy 路径的 splay 树给拿出来(但不是真的拿,也就是不是真的把边给断了拿出来)。直接把 x 换为根节点,再建一条从 yx 的实路径,把 y 旋到根,这样它就在 splay 的最右边没有右儿子了。接着就可以很方便地访问 y 来获取路径信息。

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

找根:find 函数

x 和根节点重建出一条实链后,再把 x 旋上去。然后一直走它的左儿子,因为左儿子的深度会更小,沿路 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;
}

对于连边,首先需要判断需要连边的两个点 xy 是否在同一棵子树内,在的话显然不合法。然后将 x 给改成所在树的根,再将 x 的父亲改为 y

对于断边,先把 x 改为根节点,然后判断 xy 是否有连边(即判断 xy 连通,xy 的路径上没有其他的链,y 没有右儿子),然后改一下 xy 的父亲儿子信息。

解释一下判断条件,第一条很显然,第二条可以通过判断 y 的父亲是不是 xx 的右儿子是不是 y 来解决,第三条需要满足的原因是如果 y 有左儿子,那么就会存在比 y 深度小但比 x 深度大的点,那么 xy 之前就不可能有直接的链相连。

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 函数,而它的时间复杂度是 O(log2n),那么每种操作的时间复杂度也是 O(log2n)

posted @   Cloote  阅读(93)  评论(0编辑  收藏  举报
编辑推荐:
· 从 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)
点击右上角即可分享
微信分享提示