LCT 学习笔记

一:初步认识

LCT 是一种用于解决动态树问题的数据结构。

动态树问题就是在常见的树链剖分题中加入加边和删边操作。

前置知识:Splay 树

二:实链剖分

因为动态树问题可以视为树上问题的扩展版,所以我们还是需要用树链剖分维护树上信息。

在长链剖分、重链剖分和实链剖分中,我们选择实链剖分来解决动态树问题。

实链剖分就是对于每一个节点,任选一个儿子为实儿子,其余的儿子为虚儿子;链接实儿子的边称为实边;链接虚儿子的边称为虚边。一条由实边构成的链叫做实链。

为什么我们会选择实链剖分呢?因为实链剖分最灵活且可变,你会在 LCT 的操作部分体会到这一点。

三:辅助树

动态树问题直接做不好做,因此我们要引入一个叫辅助树的东西(为了区别,我们称原来的树叫原树)。

这里有一些关于辅助树和 LCT 的一些事实:

  • 为了方便理解,你可以把辅助树理解为一些 Splay 树;而 LCT 维护的就是一个由辅助树构成的森林;

  • 在一颗辅助树中,一个 Splay 树维护原树上一条实链,且满足中序遍历这颗 Splay 数得到的序列对应原树上一条从上到下的路径;

  • 在一颗辅助树中,每一颗 Splay 树的根节点不一定为空,它指向在原树中,这颗 Splay 树对应的实链的链顶的父节点,即一条虚边;这类父亲链接满足认父不认子的性质(即父亲节点无法通过访问子节点访问到该节点)。

  • 基于上述性质,我们对原树的操作都可以转换为在辅助树上的操作。

  • 这里还有一个重要性质:由第二点性质可以得知,对于一颗 Splay 树上的每一个节点,它的左儿子代表它在原树上的父亲,右儿子代表它在原树上的实儿子。

四:LCT 的实现

一些 Splay 树的操作和基本操作

和正常的 Splay 树操作基本相同,但是在细节上有区别。

#define Get(x) (x==ch[fa[x]][1])
//依据“认父不认子”的性质判断是否为 Splay 的根节点
#define Isroot(x) (x!=ch[fa[x]][0] && x!=ch[fa[x]][1])
void push_up(int x){val[x]=val[ch[x][0]]^val[ch[x][1]]^a[x];}
void upd(int x){//打标记
    if(!x) return;
    swap(ch[x][0],ch[x][1]),tag[x]^=1;
}
void push_down(int x){//下传标记
    if(tag[x])
        upd(ch[x][0]),upd(ch[x][1]),tag[x]=0;
}
void Update(int x){//从 x 出发下传标记直到 Splay 的根。
    if(!Isroot(x)) Update(fa[x]);
    push_down(x);
}
void rotate(int x){
    int y=fa[x],z=fa[y],id=Get(x);
    //这里不同,因为根节点的父亲会指向虚边的另一个端点,因此必须通过性质判断是否为根节点。
    if(!Isroot(y)) ch[z][Get(y)]=x;
    ch[y][id]=ch[x][id^1],ch[x][id^1]=y;
    if(ch[y][id]) fa[ch[y][id]]=y;
    fa[y]=x,fa[x]=z;
    push_up(y),push_up(x);
}
void Splay(int x){
    Update(x);//将沿途会访问到的点的标记下传
    for(int y;y=fa[x],!Isroot(x);rotate(x))
        if(!Isroot(y))
            rotate(Get(x)==Get(y) ? y : x);
}

Access 操作(核心)

Access(x) 的功能是构造一条实链,满足一段为 \(x\) ,另一端为这颗辅助树的根节点。

这个操作是简单的,具体地,从 \(x\) 开始:

  • 将当前节点转移到根(因为只有在根节点才能处理虚边);

  • 将儿子(原树上的儿子,即辅助树上的右儿子)设为上一次的节点;

  • 维护当前节点信息;

  • 将当前节点变为当前节点的父亲,回到第一步;

int Access(int x){
    int p=0;
    for(p=0;x;p=x,x=fa[x])
        Splay(x),ch[x][1]=p,push_up(x);
    return p;
    //这个函数的返回值是这条实链对应的Splay的根节点,保证其父亲为空。
}

至此你已经学完 LCT 的 \(60\%\) 了。

makeroot 操作

makeroot(x) 的功能是将 \(x\) 设为当前辅助树的根。

等价于在原来根到 \(x\) 的实链上,对于每一个点,将父亲指向原来的实儿子,实儿子指向原来的父亲(好好体会一下)。

体现在 Splay 上就是交换左右儿子,用懒标记实现即可。

void makeroot(int x){
    x=Access(x),upd(x);//upd 函数的实现在前面
}

spilt 操作

spilt(x,y) 的功能是将 \(x\)\(y\) 放到一个 Splay 中,以维护 \(x\)\(y\) 这条路径。

实现不难想到,直接给代码。

void spilt(int x,int y){
    makeroot(x),Access(y),Splay(y);
}

find 操作

find(x) 的功能是找到 \(x\) 在原树中的根。

Access(x) 后,保证原树中的根就在 \(x\) 所在的 Splay 树中(因为这个节点始终没有离开过辅助树的根所在的 Splay 树),因此只需要不停跳左儿子即可(也就是在原树上不停跳父亲)。

int find(int x){
    Access(x),Splay(x);
    push_down(x);
    while(ch[x][0]) x=ch[x][0],push_down(x);
    Splay(x);
    return x;
}

link(x,y) 的功能是连边 \((x,y)\)

只需要把 \(x\) 搞到树根,并且当 Splay 的根就可以随便搞了。(当然 \(x\)\(y\) 不能在同一个树中)

void link(int x,int y){
    if(find(x)==find(y)) return;
    makeroot(x),Splay(x);
    fa[x]=y;
    //因为没有连实边,所以不需要维护节点信息,也不需要Splay(y)
}

cut 操作

cut(x,y) 的功能是删边 \((x,y)\)

spilt(x,y),然后就可以随便搞了。(当然得有边)

void cut(int x,int y){
    makeroot(x),Access(y),Splay(y);
    //这个判断条件的理解:在原树上,y的父亲是x,且x没有其他实儿子。
    if(ch[y][0]==x && !ch[x][1])
        fa[x]=ch[y][0]=0;
}
posted @ 2025-08-25 17:18  XiaoZi_qwq  阅读(18)  评论(0)    收藏  举报