【数据结构】动态树 LCT

1. 什么是动态树 (Dynamic Trees) 问题?

在许多算法问题中,我们需要维护一棵树,但这棵树的结构是动态变化的:

  • 连接 (Link):将两个原本不连通的树连接起来(添加一条边)。
  • 切断 (Cut):将一棵树断开(删除一条边)。
  • 查询路径信息:查询两点之间路径上的某些信息,例如:
    • 路径上所有点权的和。
    • 路径上的最大值/最小值。
    • 两点间的距离。
    • 两点的最近公共祖先 (LCA)。

传统的静态树数据结构(如树状数组、线段树、倍增法求LCA)无法高效处理这些动态操作。Link-Cut Tree (LCT) 就是为了解决这类动态树问题而设计的,它可以在均摊 O(log n) 的时间复杂度内完成上述所有操作。


2. LCT 的核心思想:Preferred Path (偏好路径)

LCT 的核心思想是将一棵树分解成若干条由实边 (Solid Edge) 连接的路径,这些路径称为 Preferred Path (偏好路径)。不在偏好路径上的边称为虚边 (Dashed Edge)

  • 实边 (Solid Edge):表示一个节点与其“偏好儿子” (Preferred Child) 之间的边。一个节点最多只有一个偏好儿子。
  • 虚边 (Dashed Edge):连接一个节点与其非偏好儿子,或者连接不同的偏好路径。

关键点

  • 整棵树被分解成多条由实边构成的链。
  • 每条链用一棵Splay Tree来维护。
  • 不同的链之间通过虚边连接。

偏好儿子 (Preferred Child) 如何确定?
偏好儿子通常是最后被访问的子节点。例如,当我们对某个子树进行 Access 操作时,该子节点就会成为其父节点的偏好儿子。


3. LCT 的基本操作

LCT 的所有操作都基于以下几个核心操作:

(1) Access(u)

将根节点到节点 u 的路径变为一条由实边构成的路径(即变成一条偏好路径)。

过程

  1. u Splay 到其所在 Splay 树的根。
  2. 如果 u 有右儿子(在 Splay 树中),说明存在一个原本在 u 之后的实边,现在需要将其断开(变为虚边)。将这个右儿子 v 的父节点指向 u 的指针清空,并将 v 的父节点设置为 u(通过虚边连接)。
  3. 如果 u 有父节点 p(通过虚边连接),则将 p 进行 Access 操作,然后将 p 的右儿子设为 u(将这条边变为实边),并更新 u 的父节点为 p
  4. 重复以上过程,直到到达根节点。

Access 是 LCT 最核心的操作,其他操作都依赖于它。

(2) MakeRoot(u)

将节点 u 设为整棵树的根。

过程

  1. u 进行 Access 操作,使其到原根的路径变为实边。
  2. u Splay 到其 Splay 树的根。
  3. u 对应的 Splay 树进行翻转 (Reverse) 操作(相当于将路径方向反转),这样 u 就成为了新的根。
(3) FindRoot(u)

找到节点 u 所在树的根节点。

过程

  1. u 进行 Access 操作。
  2. u Splay 到其 Splay 树的根。
  3. 在 Splay 树中,沿着左儿子一直走到底,找到最左边的节点,这个节点就是 u 所在树的根。
(4) Split(u, v)

将节点 uv 之间的路径分离出来,方便进行路径查询或更新。

过程

  1. u 设为根:MakeRoot(u)
  2. v 到根 u 的路径变为实边:Access(v)
  3. 此时 uv 在同一个 Splay 树中,且 v 是 Splay 树的根。v 的子树就代表了 uv 的路径。
(5) Link(u, v)

将节点 uv 连接起来,即添加一条边。

过程

  1. u 设为根:MakeRoot(u)
  2. u 的父节点设为 v(通过虚边连接)。在 LCT 中,这相当于将 u 作为 v 的一个儿子。
(6) Cut(u, v)

将节点 uv 之间的边断开。

过程

  1. uv 之间的路径分离:Split(u, v)
  2. 此时 uv 在同一个 Splay 树中,且 v 是根,uv 的左儿子(因为 u 是路径的起点)。
  3. u 的父节点清空,并将 v 的左儿子清空。

4. C++ 实现

下面是一个支持路径求和、路径最大值、连接、切断操作的 LCT 的 C++ 实现。

#include <iostream>
#include <algorithm>
#include <climits>
using namespace std;

const int MAXN = 100005;

struct Node {
    int val;           // 节点权值
    int sum;           // 子树权值和
    int max_val;       // 子树最大值
    int rev;           // 翻转标记
    Node* ch[2];       // 左右儿子 (0: left, 1: right)
    Node* fa;          // 父节点 (指向同一Splay中的父节点)

    Node(int v = 0) : val(v), sum(v), max_val(v), rev(0), fa(nullptr) {
        ch[0] = ch[1] = nullptr;
    }

    // 更新节点信息
    void push_up() {
        sum = val;
        max_val = val;
        for (int i = 0; i < 2; ++i) {
            if (ch[i]) {
                sum += ch[i]->sum;
                max_val = max(max_val, ch[i]->max_val);
            }
        }
    }

    // 下放翻转标记
    void push_down() {
        if (rev) {
            swap(ch[0], ch[1]);
            if (ch[0]) ch[0]->rev ^= 1;
            if (ch[1]) ch[1]->rev ^= 1;
            rev = 0;
        }
    }

    // 判断是否为所在Splay的根
    bool is_root() {
        return !fa || (fa->ch[0] != this && fa->ch[1] != this);
    }

    // 获取是父节点的哪个儿子 (0: left, 1: right)
    int get_son() {
        return fa && fa->ch[1] == this;
    }
};

Node* pool[MAXN]; // 节点池
int node_cnt = 0;

// 初始化节点
Node* new_node(int val) {
    pool[node_cnt] = new Node(val);
    return pool[node_cnt++];
}

// Splay 操作
void rotate(Node* x) {
    Node* y = x->fa;
    Node* z = y->fa;
    int d = x->get_son();

    // 更新父子关系
    if (z && !y->is_root()) z->ch[y->get_son()] = x;
    x->fa = z;

    y->ch[d] = x->ch[d ^ 1];
    if (x->ch[d ^ 1]) x->ch[d ^ 1]->fa = y;

    x->ch[d ^ 1] = y;
    y->fa = x;

    y->push_up();
    x->push_up();
}

// Splay 操作:将 x 旋转到其所在 Splay 的根
void splay(Node* x) {
    // 先将路径上的翻转标记下放
    while (!x->is_root()) {
        Node* y = x->fa;
        if (!y->is_root()) {
            y->fa->push_down();
        }
        y->push_down();
        x->push_down();
    }
    x->push_down();

    while (!x->is_root()) {
        Node* y = x->fa;
        Node* z = y->fa;
        if (!y->is_root()) {
            if (x->get_son() == y->get_son()) {
                rotate(y); // Zig-Zig
            } else {
                rotate(x); // Zig-Zag
            }
        }
        rotate(x);
    }
    x->push_up();
}

// Access 操作:将根到 x 的路径变为实边
Node* access(Node* x) {
    Node* last = nullptr;
    for (Node* y = x; y; y = y->fa) {
        splay(y);
        y->ch[1] = last; // 断开原来的右儿子(实边)
        y->push_up();
        last = y;
    }
    splay(x);
    return last;
}

// MakeRoot 操作:将 x 设为根
void make_root(Node* x) {
    access(x);
    x->rev ^= 1; // 翻转路径
    // 注意:这里 splay(x) 会自动处理翻转标记
}

// FindRoot 操作:找到 x 所在树的根
Node* find_root(Node* x) {
    access(x);
    splay(x);
    while (x->ch[0]) {
        x->push_down(); // 下放标记
        x = x->ch[0];
    }
    splay(x);
    return x;
}

// Split 操作:分离出 u 到 v 的路径
void split(Node* u, Node* v) {
    make_root(u);
    access(v);
    splay(v); // 此时 v 是 Splay 树的根,u 是 v 的左子树中的节点
}

// Link 操作:连接 u 和 v
void link(Node* u, Node* v) {
    make_root(u);
    u->fa = v;
}

// Cut 操作:切断 u 和 v 之间的边
void cut(Node* u, Node* v) {
    split(u, v);
    // 此时 u 是 v 的左儿子
    v->ch[0] = nullptr;
    u->fa = nullptr;
    v->push_up();
}

// 查询 u 到 v 路径上的权值和
int query_sum(Node* u, Node* v) {
    split(u, v);
    return v->sum;
}

// 查询 u 到 v 路径上的最大值
int query_max(Node* u, Node* v) {
    split(u, v);
    return v->max_val;
}

// 修改节点 u 的权值
void update_val(Node* u, int new_val) {
    access(u);
    splay(u);
    u->val = new_val;
    u->push_up();
}

// 释放内存 (可选)
void clear() {
    for (int i = 0; i < node_cnt; ++i) {
        delete pool[i];
    }
    node_cnt = 0;
}

// ======== 测试代码 =========
int main() {
    // 创建节点
    Node* nodes[6];
    for (int i = 1; i <= 5; ++i) {
        nodes[i] = new_node(i);
    }

    // 构建初始树: 1-2-3, 2-4, 4-5
    link(nodes[1], nodes[2]);
    link(nodes[2], nodes[3]);
    link(nodes[2], nodes[4]);
    link(nodes[4], nodes[5]);

    cout << "1 到 3 路径和: " << query_sum(nodes[1], nodes[3]) << endl; // 6 (1+2+3)
    cout << "1 到 3 路径最大值: " << query_max(nodes[1], nodes[3]) << endl; // 3

    cout << "2 到 5 路径和: " << query_sum(nodes[2], nodes[5]) << endl; // 11 (2+4+5)
    cout << "2 到 5 路径最大值: " << query_max(nodes[2], nodes[5]) << endl; // 5

    // 修改节点 4 的权值
    update_val(nodes[4], 10);
    cout << "修改后 2 到 5 路径和: " << query_sum(nodes[2], nodes[5]) << endl; // 17 (2+10+5)
    cout << "修改后 2 到 5 路径最大值: " << query_max(nodes[2], nodes[5]) << endl; // 10

    // 切断边 2-4
    cut(nodes[2], nodes[4]);
    // 重新连接 3-4
    link(nodes[3], nodes[4]);

    cout << "3 到 5 路径和: " << query_sum(nodes[3], nodes[5]) << endl; // 18 (3+10+5)
    cout << "3 到 5 路径最大值: " << query_max(nodes[3], nodes[5]) << endl; // 10

    clear();
    return 0;
}

5. 代码说明

  • Node 结构体:存储节点信息,包括权值、子树和、子树最大值、翻转标记、左右儿子和父节点。
  • push_up():合并左右子树的信息。
  • push_down():下放翻转标记。
  • is_root():判断节点是否为 Splay 的根。
  • get_son():判断是父节点的左儿子还是右儿子。
  • rotate():Splay 的基本旋转操作。
  • splay():将节点旋转到 Splay 根,注意要先下放路径上的标记。
  • access():核心操作,将根到 x 的路径变为实边。
  • make_root():通过 access + 翻转实现。
  • find_root():通过 access + splay + 向左走到底实现。
  • split():通过 make_root + access 实现路径分离。
  • link()cut():基于 make_rootsplit 实现。
  • query_sum(), query_max(), update_val():基于 splitaccess 实现查询和修改。

6. 复杂度分析

  • 时间复杂度:所有操作的均摊时间复杂度为 O(log n)
  • 空间复杂度:O(n)。

7. 应用场景

LCT 可以解决几乎所有动态树问题,例如:

  • 动态维护树的连通性。
  • 动态维护树的直径。
  • 动态维护生成树(如动态 MST)。
  • 网络流中的动态树优化。
  • 一些复杂的图论问题。

8. 注意事项

  • LCT 代码较长,细节较多,容易出错,需要仔细调试。
  • 翻转标记 (rev) 的处理非常关键,尤其是在 splayaccess 操作中。
  • access 操作后通常需要 splay 目标节点。
  • LCT 的常数较大,在 n 不是很大的情况下,可能不如其他简单方法高效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值