Link-Cut Tree

Link - Cut Tree

LCT 一般用于解决动态树问题(加边、删边)。

可以非常轻松地维护树链信息,但不太擅长维护子树信息。

Splay \(\rightarrow\) 辅助树 \(\rightarrow\) LCT

实链剖分

区别于重儿子,实儿子是我们自己选择的,于是具有极高的自由度。

LCT 通过更改实链结构达成正确的均摊复杂度。

辅助树

每条实链维护一个 Splay,中序遍历是原树上从浅到深的顺序遍历。

每条实链的 Splay 根,单向往原树上链头的父亲连虚边。

也就是说,原树上的虚边和辅助树上的虚边一一对应。

单向的意思是 儿子认父亲,父亲不认儿子

借用 OI-wiki 的图,左边是原树,右边是辅助树。

注意:

  • 辅助树的根 不等于 原树的根
  • 辅助树的 father 不等于 原树的 father
  • 辅助树可以在保证性质的前提下随意换根

函数 & 代码

splay & rotate

与普通 Splay 中略有不同

inline void rotate(int x){
    int y=fa[x],z=fa[y],sid=locate(x);
    if(!isroot(y)) son[z][locate(y)]=x;
    // 必须判断 isroot,且必须写在前面
    if(son[x][sid^1]) fa[son[x][sid^1]]=y;
    son[y][sid]=son[x][sid^1];
    son[x][sid^1]=y;
    fa[y]=x;fa[x]=z;
    pushup(y),pushup(x);
}
inline void update(int x){
    if(!isroot(x)) update(fa[x]);
    pushdown(x);
}
inline void splay(int x){
    update(x);// 先把会 rotate 到的所有节点 pushdown
    while(!isroot(x)){
        int y=fa[x];
        if(!isroot(y)) rotate(locate(x)==locate(y)?y:x);
        rotate(x);
    }
    pushup(x);
}

isroot

Splay 根向父亲连的是虚边,父亲不认儿子。
所以如果父亲的两个儿子都不是 \(x\) ,那么 \(x\) 就是 Splay 的根。

inline bool isroot(int x){
    return son[fa[x]][0]!=x && son[fa[x]][1]!=x;
}

access

LCT 核心函数,功能是 将 \(x\)原树 根的路径放到一个实链中。

我们有这样一棵树:

辅助树可能是这样:

现在要 access(N) 把 A - N 的路径放到一个 Splay 里,就是要把虚边变成实边,原来的实边变成虚边。

根据辅助树的定义,实儿子一定是中序遍历上右边相邻的点。

实现过程:

  • 把 N 旋转到当前 Splay 的根
  • N 连的实边要变成虚边,即把 Splay 上右儿子设为 0

此时变成了这样:

  • 把 N 的 father I 旋转到 Splay 的根
  • I 之前连的实边要变成虚边,现在向 L 连的边变成实边,即把 Splay 上 I 的右儿子变成 N

此时得到了 I - L 的 Splay

同理对 H 和 A 进行操作,就能把 A - N 的路径都放进一个 Splay

inline void access(int x){
    for(int y=0;x;y=x,x=fa[x])
        splay(x),son[x][1]=y,pushup(x);
    return $x$ ;
}

makeroot

\(x\) 变成 原树 的根。

相当于是把 \(x\) 到本来的根的路径区间翻转。

access(x),此时 \(x\) 到根的路径放到了同一个 Splay。
然后 Splay(x),再给 \(x\) 打一个翻转 tag,就完成了路径翻转。

inline void pushr(int x){
    swap(son[x][0],son[x][1]);
    rev[x]^=1;
}
inline void makeroot(int x){
    access(x),splay(x);
    pushr(x);
}

findroot

找到 \(x\) 所在 原树 的根。

access 再 splay 后,原树的根就是最左边的节点。

inline int findroot(int x){
    access(x),splay(x);
    while(son[x][0]) pushdown(x),x=son[x][0];
    splay(x);
    return $x$ ;
}

\(x\) 转到根,然后让 \(x\) 指向 \(y\) 即可。

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

cut

需要判断原来有边。

钦定 \(x\) 为根,先判断是否在一棵树里,然后判断 \(x,y\) 是否直接连边。
要求 \(y\) 的左子树没有点(如果有,说明在 $x\to y $ 的链上,即 \(x,y\) 没有直接连边)。

inline void cut(int x ,int y){
    makeroot(x);
    if(findroot(y)==x && fa[y]==x && !son[y][0])
        son[x][1]=0,fa[y]=0,pushup(x);
}

split

把 x 到 y 的路径拆出来,简单平凡。

inline void split(int x,int y){
    makeroot(x),access(y),splay(y);
}
posted @ 2025-02-19 16:03  Cindy_Li  阅读(64)  评论(0)    收藏  举报