【Coel.学习笔记】连切动态树(Link-Cut-Tree)

终于到臭名昭著的连切树了,它也是整个大纲里最难的树……

引入

动态树是一类维护森林的问题,能够在树链剖分的基础上解决动态断连边等修改操作。其中,连切树(\(\text{Link-Cut-Tree}\),以下简称 LCT)为求解动态树问题的一种方式,利用 Splay 实现,发明者为 Sleator 与 Tarjan(嗯,又是他)。
动态树问题还可以使用欧拉环游树、拓扑树等数据结构解决,但连切树是其中最常用的一种。
LCT 的时间复杂度为 \(O(\log n)\),优于树链剖分的 \(O(\log ^2n)\),但常数较大。

概念与性质

LCT 的边可以分成虚边和实边。一个点最多只有一条实边,也可以没有。实边连成的路径叫做实边路径,单独的一个点也叫实边路径。实边与虚边的差别仅在于树链剖分时是否选择了这条边。

  1. 我们使用 Splay 维护实边路径,每一个 Splay 的中序遍历为它要维护的路径;Splay 维护的路径一定为极大路径。此时 Splay 具有中序遍历按深度递增的性质。
  2. Splay 的前驱与后继维护 LCT 的父子关系。注意:Splay 和 LCT 不是同一颗树
  3. 对于虚边,它一定是实边路径最高点与其父节点的连边。由于 Splay 的根节点一定没有父节点,所以虚边可以直接用根节点维护。此时,只要让最高的实边路径对应的 Splay 根节点指向 LCT 的根节点,其余 Splay 的根节点指向与其相连的虚边向上的点即可。

核心操作

操作太多,所以先写文字,等下放整个代码。

\(\text{access}\) 实边路径建立

这个操作可以使 \(x\) 节点与根节点建立实边路径,即把路径上所有边改成实边,对应的边改成虚边。
先把 \(x\) 节点伸展到根节点,然后更换儿子、更新信息,然后对实边父亲执行这个操作,直到路径建立完成。

\(\text{makeroot}\) 更换根节点

\(x\) 换成根节点。
先建立实边路径,这时的 \(x\) 是深度最大的点。随后把 \(x\) 旋转到根节点,翻转整个 Splay 对应的区间。反转后 \(x\) 成了深度最小的点,也就是根节点了。

\(\text{findroot}\) 寻找根节点

找到 \(x\) 所在树的根节点。
先建立实边路径,然后将其旋转到根节点。接下来一直找左儿子,直到左儿子不存在。这时找到的点就是根节点。

\(\text{split}\) 将路径建立为 Splay

传入两个节点 \(x,y\),将它们之间的路径变为实边路径,再把路径建立为 Splay,根节点为 \(y\)
先通过 makeroot 把 \(x\) 变成根节点,然后建立从根到 \(y\) 的实边路径。

\(\text{link}\) 加边连接

加入边 \((x,y)\)(假设它们不连通)。
先把 \(x\) 换成根节点,然后找到 \(y\) 所在根节点。若为 \(x\) 则两点连通,无需操作;反之让 \(x\) 的父节点变为 \(y\)

\(\text{cut}\) 删边断开

删除边 \((x,y)\)(假设边存在)。
还是先把 \(x\) 变为根节点,找 \(y\) 所在根节点。然后判断 \(y\) 是否为 \(x\) 的后继,若不是则不存在边,无需删除;反之让 \(x\) 的父节点为空。

\(\text{isroot}\) 判断是否为根

判断节点 \(x\) 是否为所在 Splay 的根节点。
反过来想,如果不是根节点,那么 \(x\) 必然是其父节点的左儿子或右儿子。这样,只需要判断其父节点的左儿子或右儿子是不是 \(x\)

例题讲解

【模板】动态树

洛谷传送门
给定 \(n\) 个点以及每个点的权值,处理接下来的 \(m\) 个操作。
操作有四种,操作从 \(0\)\(3\) 编号。点从 \(1\)\(n\) 编号。

  • 0 x y 代表询问从 \(x\)\(y\) 的路径上的点的权值的异或和。保证 \(x\)\(y\) 是联通的。
  • 1 x y 代表连接 \(x\)\(y\),若 \(x\)\(y\) 已经联通则无需连接。
  • 2 x y 代表删除边 \((x,y)\),不保证边 \((x,y)\) 存在。
  • 3 x y 代表将点 \(x\) 上的权值变成 \(y\)

解析:在 Splay 中维护翻转懒标记和异或和,直接按照 LCT 的操作进行即可。
对于修改操作,先把 \(x\) 伸展到根。由于父节点信息改变不影响子节点,所以修改时只改 \(x\) 的信息,做一遍信息汇总即可。
那么,代码如下:

#include <algorithm>
#include <cstring>
#include <iostream>

using namespace std;

const int maxn = 1e5 + 10;

int n, m;
struct node {
    int ch[2], p, v;
    int sum, rev;
} t[maxn];
int stk[maxn];

void pushrev(int x) { //下传翻转标记
    swap(t[x].ch[0], t[x].ch[1]);
    t[x].rev ^= 1;
}

void pushup(int x) {
    t[x].sum = t[t[x].ch[0]].sum ^ t[x].v ^ t[t[x].ch[1]].sum;
}

void pushdown(int x) {
    if (t[x].rev) {
        pushrev(t[x].ch[0]);
        pushrev(t[x].ch[1]);
        t[x].rev = 0;
    }
}

bool isroot(int x) { return t[t[x].p].ch[0] != x && t[t[x].p].ch[1] != x; }

void rotate(int x) {
    int y = t[x].p, z = t[y].p;
    int k = (t[y].ch[1] == x);
    if (!isroot(y)) t[z].ch[t[z].ch[1] == y] = x;
    t[x].p = z;
    t[y].ch[k] = t[x].ch[k ^ 1], t[t[x].ch[k ^ 1]].p = y;
    t[x].ch[k ^ 1] = y, t[y].p = x;
    pushup(y), pushup(x);
}

void splay(int x) {
    int top = 0, r = x;
    stk[++top] = r;
    while (!isroot(r)) stk[++top] = r = t[r].p;
    while (top) pushdown(stk[top--]);
    //以上为 LCT 特有的伸展前置操作
    /*先把路径上的点全部 pushdown 才能旋转
    由于直接写递归不太美观(而且递归常数大)
    所以用一个栈来存要 pushdown 的点*/
    while (!isroot(x)) {
        int y = t[x].p, z = t[y].p;
        if (!isroot(y)) {
            if ((t[y].ch[1] == x) ^ (t[z].ch[1] == y))
                rotate(x);
            else
                rotate(y);
        }
        rotate(x);
    }
}

//以上为 Splay 操作

void access(int x) {
    int z = x;
    for (int y = 0; x; y = x, x = t[x].p) {
        splay(x);
        t[x].ch[1] = y;
        pushup(x);
    }
    splay(z);
}

void makeroot(int x) {
    access(x);
    pushrev(x);
}

int findroot(int x) {
    access(x);
    while (t[x].ch[0]) {
        pushdown(x);
        x = t[x].ch[0];
    }
    splay(x);
    return x;
}

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

void link(int x, int y) {
    makeroot(x);
    if (findroot(y) != x) t[x].p = y;
}

void cut(int x, int y) {
    makeroot(x);
    if (findroot(y) == x && t[y].p == x && !t[y].ch[0]) {
        t[x].ch[1] = t[y].p = 0;
        pushup(x);
    }
}

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> t[i].v;
    while (m--) {
        int op, x, y;
        cin >> op >> x >> y;
        if (op == 0)
            split(x, y), cout << t[y].sum << '\n';
        else if (op == 1)
            link(x, y);
        else if (op == 2)
            cut(x, y);
        else
            splay(x), t[x].v = y, pushup(x);
    }
    return 0;
}

操作很多,但其实很多操作只要两个函数就可以过去,所以码量并不大。

A + B Problem(?

洛谷传送门 双倍经验 三倍经验
输入两个整数 \(a, b\),输出它们的和(\(|a|,|b| \le {10}^9\))。

注意

  1. Pascal 使用 integer 会爆掉哦!
  2. 有负数哦!
  3. C/C++ 的 main 函数必须是 int 类型,而且 C 最后要 return 0。这不仅对洛谷其他题目有效,而且也是 NOIP/CSP/NOI 比赛的要求!

好吧,同志们,我们就从这一题开始,向着大牛的路进发。

任何一个伟大的思想,都有一个微不足道的开始。

解析:嗯,纯粹没事做,顺便致敬用 LCT 写题解的 Treeloveswater 先生。
先给 \(a,b\) 连边,然后求它们的路径和。别忘了信息汇总的时候把异或和改成加和。
为了更好的致敬这个大佬,我们在实际中也用连-切-连的脑瘫操作。
主函数代码如下:

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> t[1].v >> t[2].v;
    link(1, 2), cut(1, 2), link(1, 2);
    split(1, 2);
    cout << t[2].sum;
    return 0;
}

[SDOI2008] 洞穴勘测

洛谷传送门
给定一个无向无权图,进行如下操作:

  • Connect u v\(u,v\) 连边。
  • Destory u v 删除边 \(u,v\)
  • Query u v 查询 \(u,v\) 是否连通,连通输出 Yes,否则输出 No

解析:板啊!很板啊!
甚至不需要维护权值, pushup 再您的见!

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    while (m--) {
        char op[30];
        int u, v;
        cin >> op >> u >> v;
        if (!strcmp(op, "Connect"))
            link(u, v);
        else if (!strcmp(op, "Destroy"))
            cut(u, v);
        else {
            if (findroot(u) == findroot(v))
                cout << "Yes" << '\n';
            else
                cout << "No" << '\n';
        }
    }
    return 0;
}

下一题是比较正经的动态树例题了……

[NOI2014] 魔法森林

洛谷传送门
给定一张无向图,每条边都有两个权值 \(a_i,b_i\)。找到一条从起点到终点的路径,使得路径上的 \(a_i\) 最大值与 \(b_i\) 最大值之和最小。

解析:看起来有点像最短路,但可惜不能用最短路做。

考虑枚举 \(a_i\),对于枚举权值的最大值 \(A\),找到 \(b_i\) 对应的最大值和最小值。由于 \(A\) 已经确定,所以我们能使用的边必须满足 \(a_i\leq A\),这样枚举有了方向。

我们先对 \(a_i\) 做一个从小到大的排序(对 \(b_i\) 排序也行),然后逐个枚举 \(A\),与此同时将满足条件的边加入图中,并求出 \(b_i\) 最大值的最小化。当图中出现环时,我们要去掉权值最大的一条边,从而保证最大值最小(类似 Kruskal 的思想)。

去掉最大权值边的操作看起来有点“贪心”,会不会导致错失最优解呢?答案是否定的。利用反证法,假设去掉环上最大边后无法得到最优解,那么这条边换成环内另外几条边时,这个环对应的权值会变大,这与权值最大矛盾,因此去掉最大权值边可以得到最优解。

利用 Splay 可以轻松地动态维护最小值;同时我们要动态地插入、删除边,很容易想到用 LCT。由于权值在边上而 LCT 维护的权值在点上,所以类比网络流,使用“拆边”的技巧,在边上建立一个点,把边权赋给点权。此外判断连通时用 findroot 常数比较大,我们改用并查集维护。

下面是 pushup 函数和主函数的代码:

void pushup(int x) {
    t[x].mx = x; //mx 维护子树中权值最大点
    for (int i = 0; i < 2; i++)
        if (t[t[t[x].ch[i]].mx].v > t[t[x].mx].v) t[x].mx = t[t[x].ch[i]].mx;
}

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y, u, v;
        cin >> x >> y >> u >> v;
        e[i] = {x, y, u, v};
    }
    sort(e + 1, e + m + 1);
    for (int i = 1; i <= n + m; i++) {
        f[i] = i;
        if (i > n) t[i].v = e[i - n].b;  //拆边
        t[i].mx = i;
    }
    for (int i = 1; i <= m; i++) {
        int u = e[i].u, v = e[i].v, a = e[i].a, b = e[i].b;
        if (find(u) == find(v)) {
            split(u, v);
            int mx = t[v].mx;
            if (t[mx].v > b) {  //有环且存在更小权值,删边并维护更小权值的边
                cut(e[mx - n].u, mx), cut(mx, e[mx - n].v);
                link(u, n + i), link(n + i, v);
            }
        } else {  //不连通,直接连边
            f[find(u)] = find(v);
            link(u, n + i), link(n + i, v);
        }
        if (find(1) == find(n)) {  //可以到达终点则更新答案
            split(1, n);
            res = min(res, t[t[n].mx].v + a);
        }
    }
    if (res == inf) {
        cout << -1;
        goto Miolic_End;
    }
    cout << res;
Miolic_End:
    return 0;
}

[国家集训队]Tree II

给定一棵初始权值均为 \(1\) 的树,进行以下四个操作:
洛谷传送门

  • + u v c:将 \(u\)\(v\) 的路径上的点的权值都加上自然数 \(c\)
  • - u1 v1 u2 v2:将树中原有的边 \((u_1,v_1)\) 删除,加入一条新边 \((u_2,v_2)\),保证操作完之后仍然是一棵树;
  • * u v c:将 \(u\)\(v\) 的路径上的点的权值都乘上自然数 \(c\)
  • / u v:询问 \(u\)\(v\) 的路径上的点的权值和,将答案对 \(51061\) 取模。

解析:如果没有乘法操作,这题就是一个裸的动态树问题,直接用上面 A + B Problem 的做法即可。
现在有了乘法操作,关键就在于标记下传。类比【模板】线段树 2 的做法,我们在每次下传标记的时候先传乘法再传加法,最后翻转。由于 Splay 的长度不固定,所以我们还要再维护一个 size。
剩下的就是 LCT 的板子了,注意一点细节即可。这里用了类封装 LCT,看起来更简洁一点……吧?

#include <algorithm>
#include <iostream>

#define int unsigned int //答案会爆 int,但不会爆 unsigned int……

using namespace std;

const int maxn = 5e5 + 10, mod = 51061;

int n, Q;

class Link_Cut_Tree {
   private:
    struct node {
        int ch[2], p, v;
        int sum, size;
        int rev, add, mul;
    } t[maxn];
    int stk[maxn];

    void pushup(int x) {
        t[x].sum = (t[t[x].ch[0]].sum + t[t[x].ch[1]].sum + t[x].v) % mod;
        t[x].size = t[t[x].ch[0]].size + t[t[x].ch[1]].size + 1;
    }

    void pushrev(int x) {
        swap(t[x].ch[0], t[x].ch[1]);
        t[x].rev ^= 1;
    }

    void pushadd(int x, int c) { //下传加法
        (t[x].sum += c * t[x].size) %= mod;
        (t[x].v += c) %= mod;
        (t[x].add += c) %= mod;
    }

    void pushmul(int x, int c) { //下传乘法
        (t[x].sum *= c) %= mod;
        (t[x].v *= c) %= mod;
        (t[x].mul *= c) %= mod;
        (t[x].add *= c) %= mod;
    }

    void pushdown(int x) { //先乘后加再翻转
        if (t[x].mul != 1) {
            pushmul(t[x].ch[0], t[x].mul);
            pushmul(t[x].ch[1], t[x].mul);
            t[x].mul = 1;
        }
        if (t[x].add) {
            pushadd(t[x].ch[0], t[x].add);
            pushadd(t[x].ch[1], t[x].add);
            t[x].add = 0;
        }
        if (t[x].rev) {
            if (t[x].ch[0]) pushrev(t[x].ch[0]);
            if (t[x].ch[1]) pushrev(t[x].ch[1]);
            t[x].rev ^= 1;
        }
    }

    bool isroot(int x) { return t[t[x].p].ch[0] != x && t[t[x].p].ch[1] != x; }

    void rotate(int x) {
        int y = t[x].p, z = t[y].p;
        int k = (t[y].ch[1] == x);
        if (!isroot(y)) t[z].ch[t[z].ch[1] == y] = x;
        t[x].p = z;
        t[y].ch[k] = t[x].ch[k ^ 1], t[t[x].ch[k ^ 1]].p = y;
        t[x].ch[k ^ 1] = y, t[y].p = x;
        pushup(y), pushup(x);
    }

    void splay(int x) {
        int top = 0, r = x;
        stk[++top] = r;
        while (!isroot(r)) stk[++top] = r = t[r].p;
        while (top) pushdown(stk[top--]);
        while (!isroot(x)) {
            int y = t[x].p, z = t[y].p;
            if (!isroot(y)) {
                if ((t[y].ch[1] == x) ^ (t[z].ch[1] == y))
                    rotate(x);
                else
                    rotate(y);
            }
            rotate(x);
        }
    }

    void access(int x) {
        int z = x;
        for (int y = 0; x; y = x, x = t[x].p) {
            splay(x);
            t[x].ch[1] = y;
            pushup(x);
        }
        splay(z);
    }

    void makeroot(int x) {
        access(x);
        pushrev(x);
    }

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

    int findroot(int x) {
        access(x);
        while (t[x].ch[0]) {
            pushdown(x);
            x = t[x].ch[0];
        }
        splay(x);
        return x;
    }

   public:
    void init(int n) { //初始化,注意乘法标记要初始化为 1
        for (int i = 1; i <= n; i++) t[i].mul = t[i].size = t[i].v = 1;
    }

    void link(int x, int y) {
        makeroot(x);
        if (findroot(y) != x) t[x].p = y;
    }

    void cut(int x, int y) {
        makeroot(x);
        if (findroot(y) == x && t[y].p == x && !t[y].ch[0]) {
            t[x].ch[1] = t[y].p = 0;
            pushup(x);
        }
    }

    void add(int u, int v, int c) { split(u, v), pushadd(v, c); }

    void mul(int u, int v, int c) { split(u, v), pushmul(v, c); }

    int query(int u, int v) { return split(u, v), t[v].sum; }

} LCT;

signed main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> Q;
    LCT.init(n);
    for (int i = 1, u, v; i <= n - 1; i++) {
        cin >> u >> v;
        LCT.link(u, v);
    }
    while (Q--) {
        char op;
        cin >> op;
        if (op == '+') {
            int u, v, c;
            cin >> u >> v >> c;
            LCT.add(u, v, c);
        } else if (op == '-') {
            int u1, v1, u2, v2;
            cin >> u1 >> v1 >> u2 >> v2;
            LCT.cut(u1, v1), LCT.link(u2, v2);
        } else if (op == '*') {
            int u, v, c;
            cin >> u >> v >> c;
            LCT.mul(u, v, c);
        } else {
            int u, v;
            cin >> u >> v;
            cout << LCT.query(u, v) << '\n';
        }
    }
    return 0;
}
posted @ 2022-08-03 22:15  秋泉こあい  阅读(50)  评论(0编辑  收藏  举报