LCT

LCT

树链剖分

常见的三种:重链、长链、轻实链。LCT 采用的是轻实链剖分。

对于树上每一个点,将某一儿子作为实儿子,注意只有一个

连向其的边设为实边,连向其他子树的边设为虚边。

轻实边需要随着树形态的变化而改变

LCT 支持如下操作:

  • 维护数链信息
  • 换根
  • 联通性
  • 动态连边、删边

有了实儿子,还有两个定义:

  • 实边,连接父亲和实儿子的边。

  • 实链,实边构成的链。

采用 splay 作为 LCT 的辅助树,有如下定义和性质

  • 一条实链的辅助树是维护这条链上节点的 splay

    每一颗 splay 维护一条从上到下深度严格递增的路径

  • 按照节点原树中的深度排序,可得中序遍历 splay 得到的点深度严格递增。

  • 一个节点仅在一棵 splay

  • 虚边总是由一棵 splay 指向另一个节点

    重要性质:认父不认子。

    即对于一个点,不会在 splay 中记录它的虚儿子,但是会在 splay 中记录它虚儿子的父亲为这个点

实现

平衡树

实现其实是实现一个 splay 森林,支持区间翻转操作,点中编号就是原树的编号

因为多个实链多个根,用函数 is_root 判断是否为根。(不过我习惯用 not_root

inline int nRt(int x) { return ch[fa[x]][0] == x || ch[fa[x]][1] == x; }

splay 函数需要先将点 x 到根路径上的点从上到下 pushdown 一下,因为这里没有查询 kth 的操作。

注意 pushup

pushdown 也有讲究:当一个点打标记时,需保证左右儿子都在正确的位置上。

inline void Up(int x) {
    // push up
}
inline void rev(int x) { swap(ch[x][0], ch[x][1]), tg[x] ^= 1; }
inline void down(int x) { if (tg[x]) rev(ch[x][0]), rev(ch[x][1]), tg[x] = 0; }
inline int get(int x) { return ch[fa[x]][1] == x; }
inline int nRt(int x) { return ch[fa[x]][0] == x || ch[fa[x]][1] == x; }
inline void rot(int x) {
    int y = fa[x], z = fa[y], k = get(x);
    if (nRt(y)) ch[z][get(y)] = x; // important
    fa[x] = z;
    ch[y][k] = ch[x][k ^ 1], fa[ch[x][k ^ 1]] = y;
    ch[x][k ^ 1] = y, fa[y] = x;
    Up(y), Up(x);
}
inline void upd(int x) {
    st[top = 1] = x;
    while (nRt(x)) st[++top] = x = fa[x];
    while (top) down(st[top--]);
}
inline void splay(int x) {
    upd(x); // important
    for (int y; y = fa[x], nRt(x); rot(x))
        if (nRt(y)) rot(get(x) ^ get(y) ? x : y);
}

access

LCT 的核心操作, access(x)\(x\) 到原树根路径上的边全部变成实边。

并且 \(x\) 为这一条实链的最后一个点,深度最大。

\(x\) 与原先实儿子切断,\(x\) 到原树根路径上所有点构成一个 splay ,便于操作、查询

inline void access(int x) {
    for (int y = 0; x; x = fa[y = x])
        splay(x), ch[x][1] = y, Up(x);
}

做法:不断将 \(x\) 旋转到当前 splay 的根,它的右儿子就是重儿子,需要修改。用变量 \(y\) 辅助

注意: access\(x\) 不一定为所得 splay 的根

makeroot

\(x\) 设为原树树根。

inline void make(int x) {
    access(x), splay(x), rev(x);
}

access + splay\(x\) 转到了 splay 的根。

此时 \(x\) 的深度最大,打上翻转标记后深度就最小了,成为了根

findroot

找到 \(x\) 在原树中的根

inline int Find(int x) {
    access(x), splay(x), down(x);
    while (ch[x][0]) x = ch[x][0], down(x);
    splay(x); // remember
    return x;
}

同理, access + splay\(x\) 深度最大,在 splay 的根节点。

一直往左儿子走找到深度最小的点就是根节点。

注意最后的 splay ,将原树根转回 splay 的根。等下会提到

split

split(x, y) 得到原树中 \(x\)\(y\) 路径上的点组成的 splay ,并且 \(y\) 为根

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

先将 \(x\) 设为根,然后 access + splay 得到 \(y\)\(x\) 的路径

连一条 \((x,y)\) 的轻边。

inline void link(int x, int y) {
    make(x);
    if (Find(y) ^ x) fa[x] = y;
}

cut

断掉边 \((x,y)\)

inline void cut(int x, int y) {
    make(x);
    if (Find(y) == x && fa[y] == x && !ch[y][0])
        fa[y] = ch[x][1] = 0, Up(x);
}

注意判断的三个条件,确定存在边 \((x,y)\)

  • 联通,将 \(x\) 设置为根。
  • splay 中父子关系,具体一点 \(y\)\(x\) 的右儿子
  • \(x,y\) 之间没有点,即 \(y\) 没有左儿子

此时体现在 findroot 中最后 splay 的作用了。将 splay 的根还原,再继续操作

在一些写法中是没有这个操作的,导致后续的两个判断有些差异,单看这一个函数会觉得莫名其妙。

edge

实际上,判断是否存在边 \((x,y)\) 是非常常用的函数。

edge(x, y) 判断同一个树中,是否存在边 \((x,y)\) ,借助 split(x, y) 实现

inline bool edge(int x, int y) {
    split(x, y);
    return ch[y][0] == x && !ch[x][1];
}

code 模板

模板

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 100005;
int n, Ti, a[N], xo[N], ch[N][2], fa[N], tg[N], st[N], top;
inline void Up(int x) { xo[x] = xo[ch[x][0]] ^ xo[ch[x][1]] ^ a[x]; }
inline void rev(int x) { swap(ch[x][0], ch[x][1]), tg[x] ^= 1; }
inline void down(int x) { if (tg[x]) rev(ch[x][0]), rev(ch[x][1]), tg[x] = 0; }
inline int get(int x) { return ch[fa[x]][1] == x; }
inline int nRt(int x) { return ch[fa[x]][0] == x || ch[fa[x]][1] == x; }
inline void rot(int x) {
    int y = fa[x], z = fa[y], k = get(x);
    if (nRt(y)) ch[z][get(y)] = x;
    fa[x] = z;
    ch[y][k] = ch[x][k ^ 1], fa[ch[x][k ^ 1]] = y;
    ch[x][k ^ 1] = y, fa[y] = x;
    Up(y), Up(x);
}
inline void upd(int x) {
    st[top = 1] = x;
    while (nRt(x)) st[++top] = x = fa[x];
    while (top) down(st[top--]);
}
inline void splay(int x) {
    upd(x);
    for (int y; y = fa[x], nRt(x); rot(x))
        if (nRt(y)) rot(get(x) ^ get(y) ? x : y);
}
inline void access(int x) {
    for (int y = 0; x; x = fa[y = x])
        splay(x), ch[x][1] = y, Up(x);
}
inline void make(int x) {
    access(x), splay(x), rev(x);
}
inline int Find(int x) {
    access(x), splay(x), down(x);
    while (ch[x][0]) x = ch[x][0], down(x);
    splay(x);
    return x;
}
inline void split(int x, int y) {
    make(x), access(y), splay(y);
}
inline void link(int x, int y) {
    make(x);
    if (Find(y) ^ x) fa[x] = y;
}

inline void cut(int x, int y) {
    make(x);
    if (Find(y) == x && fa[y] == x && !ch[y][0])
        fa[y] = ch[x][1] = 0, Up(x);
}

int main() {
    scanf("%d%d", &n, &Ti);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]), xo[i] = a[i];
    for (int op, x, y; Ti--; ) {
        scanf("%d%d%d", &op, &x, &y);
        if (op == 0) split(x, y), printf("%d\n", xo[y]);
        else if (op == 1) link(x, y);
        else if (op == 2) cut(x, y);
        else if (op == 3) splay(x), a[x] = y, Up(x);
    }
}

应用

维护联通性

比较简单。直接用 findroot 判断。

[SDOI2008] 洞穴勘测

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 10005;
char buf[100000], *S = buf, *T = buf;
inline char G() {
    return S == T && (T = (S = buf) + fread(buf, 1, 100000, stdin), S == T) ? EOF : *S++;
}
inline int Rd() {
    register int x = 0;
    char C = G();
    for (; C < '0' || C > '9'; C = G()) ;
    for (; C > '/' && C < ':'; C = G()) x = (x << 1) + (x << 3) + (C ^ 48);
    return x;
}
int n, TT;
int st[N], top;
int fa[N], ch[N][2], tg[N];
char op;
/*
LCT here
*/
int main() {
    n = Rd(), TT = Rd();
    for (int x, y; TT--; ) {
        op = G();
        while (op < 'A' || op > 'Z') op = G();
        x = Rd(), y = Rd();
        if (op == 'C') link(x, y);
        else if (op == 'D') cut(x, y);
        else puts(Find(x) == Find(y) ? "Yes" : "No");
    }
}

维护树链信息

比较简单,用 split 结合各种标记维护。

[国家集训队]Tree II

注意懒标记优先级。由于题目保证联通,link,cut 可以不判断联通性

同时这是国集的题,数据很强,如果硬要在判断联通性,一定不能打假,否则可能调一上午。。。

#include <bits/stdc++.h>
#define int long long
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 100005;
const LL P = 51061;
int n, T, ch[N][2], fa[N], st[N], top;
LL val[N], sm[N], tg[N], ad[N], mu[N], sz[N];
char op[5];
inline int get(int x) { return ch[fa[x]][1] == x; }
inline int nRt(int x) { return ch[fa[x]][0] == x || ch[fa[x]][1] == x; }
inline void Up(int x) {
    sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + 1;
    sm[x] = (sm[ch[x][0]] + sm[ch[x][1]] + val[x]) % P;
}
inline void rev(int x) { swap(ch[x][0], ch[x][1]), tg[x] ^= 1; }
inline void mul(int x, LL v) {
    (mu[x] *= v) %= P;
    (ad[x] *= v) %= P;
    (sm[x] *= v) %= P;
    (val[x] *= v) %= P;
}
inline void add(int x, LL v) {
    (ad[x] += v) %= P;
    (sm[x] += v * sz[x]) %= P;
    (val[x] += v) %= P;
}
inline void down(int x) {
    if (mu[x] != 1) mul(ch[x][0], mu[x]), mul(ch[x][1], mu[x]), mu[x] = 1;
    if (ad[x]) add(ch[x][0], ad[x]), add(ch[x][1], ad[x]), ad[x] = 0;
    if (tg[x]) {
        if (ch[x][0]) rev(ch[x][0]);
        if (ch[x][1]) rev(ch[x][1]);
        tg[x] = 0;
    }
}
inline void rot(int x) {
    int y = fa[x], z = fa[y], k = get(x);
    if (nRt(y)) ch[z][get(y)] = x;
    fa[x] = z;
    if (ch[x][k ^ 1]) fa[ch[x][k ^ 1]] = y;
    ch[y][k] = ch[x][k ^ 1];
    ch[x][k ^ 1] = y, fa[y] = x;
    Up(y), Up(x);
}
inline void upd(int x) {
    st[top = 1] = x;
    while (nRt(x)) st[++top] = x = fa[x];
    while (top) down(st[top--]);
}
inline void splay(int x) {
    upd(x);
    for (int y; y = fa[x], nRt(x); rot(x))
        if (nRt(y)) rot(get(x) ^ get(y) ? x : y);
}
inline void access(int x) { for (int y = 0; x; x = fa[y = x]) splay(x), ch[x][1] = y, Up(x); }
inline void make(int x) { access(x), splay(x), rev(x); }
inline int Find(int x) {
    access(x), splay(x), down(x);
    while (ch[x][0]) x = ch[x][0], down(x);
    splay(x);
    return x;
}
inline void link(int x, int y) {
    make(x);
    if (Find(y) != x) fa[x] = y;
}
inline void cut(int x, int y) {
    make(x);
    if (Find(y) == x && fa[y] == x && !ch[y][0])
        fa[y] = ch[x][1] = 0, Up(x);
}
inline void split(int x, int y) { make(x), access(y), splay(y); }
signed main() {
    // freopen("1.in", "r", stdin);
    // freopen("1.out", "w", stdout);
    scanf("%lld%lld", &n, &T);
    for (int i = 1; i <= n; i++) val[i] = mu[i] = sz[i] = 1;
    for (int i = 1, u, v; i < n; i++) {
        scanf("%lld%lld", &u, &v);
        link(u, v);
    }
    for (int u, v, w; T--; ) {
        scanf("%s%lld%lld", op, &u, &v);
        if (op[0] == '*') {
            scanf("%lld", &w);
            split(u, v);
            mul(v, 1ll * w);
        } else if (op[0] == '+') {
            scanf("%lld", &w);
            split(u, v);
            add(v, 1ll * w);
        } else if (op[0] == '/') {
            split(u, v);
            printf("%lld\n", sm[v]);
        } else if (op[0] == '-') {
            cut(u, v);
            scanf("%lld%lld", &u, &v);
            link(u, v);
        }
    }
}

维护生成树

重要。

最小生成树 为例,用类似 kruskal 的思路就是依次加入边

如果加入边 \((u,v,w)\) 形成了环,就选出 \((u,v)\) 链上边权最大的一条边 \((u',v',w')\)

如果满足 \(w<w'\) 就删除边 \((u',v')\) 并加入边 \((u,v,w)\) ,否则不加入。

加入所有边后即可得到最小生成树,无需对边权排序

由于 lct 的形态会不断变化,难以直接维护边权。处理方法是拆边。

将边权放在代表边的点权里,代表点的点权设为 0 。这样可以只维护点权,是可以实现的操作。

#include <bits/stdc++.h>
using namespace std;
const int N = 250005;
int n, m, a[N], id[N], mx[N], fa[N], ch[N][2], st[N], top, tg[N], tot, ans, use;
inline void Up(int x) {
    mx[x] = a[x], id[x] = x;
    if (mx[x] < mx[ch[x][0]]) mx[x] = mx[ch[x][0]], id[x] = id[ch[x][0]];
    if (mx[x] < mx[ch[x][1]]) mx[x] = mx[ch[x][1]], id[x] = id[ch[x][1]];
}
/*
LCT here
*/
inline int chk(int x, int y) { return make(x), Find(y) == x; }
int main() {
    scanf("%d%d", &n, &m);
    tot = n;
    for (int i = 1, u, v, w, nw; i <= m; i++) {
        ++tot;
        scanf("%d%d%d", &u, &v, &w), a[tot] = w;
        if (!chk(u, v)) link(u, tot), link(v, tot), ans += w, ++use;
        else {
            split(u, v);
            if (mx[v] <= w) continue;
            nw = id[v];
            ans += w - mx[v], splay(nw);
            fa[ch[nw][0]] = fa[ch[nw][1]] = 0;
            ch[nw][0] = ch[nw][1] = 0;
            link(u, tot), link(v, tot);
        }
    }
    if (use == n - 1) printf("%d", ans);
    else puts("orz");
}

例 魔法森林

魔法森林

边权由 \((a,b)\) 表示,要求从 1 到 \(n\) 路径上(( \(a\) 的最大值)和( \(b\) 的最大值)之和)最小。

按照 \(b\) 排序,维护 \(a\) 的最小生成树。

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 200005;
int n, m, ff[N], val[N], id[N], fa[N], ch[N][2], st[N], tg[N], ans = 2e9;
struct Edge { int u, v, a, b; } e[N];
inline bool cmp(Edge A, Edge B) {
    return A.b < B.b;
}
inline int gmx(int x, int y) { return val[x] > val[y] ? x : y; }
inline void Up(int x) { id[x] = gmx(x, gmx(id[ch[x][0]], id[ch[x][1]])); }
inline int get(int x) { return ch[fa[x]][1] == x; }
inline bool nRt(int x) { return ch[fa[x]][0] == x || ch[fa[x]][1] == x; }
inline void rev(int x) { tg[x] ^= 1, swap(ch[x][0], ch[x][1]); }
inline void down(int x) { if (tg[x]) rev(ch[x][0]), rev(ch[x][1]), tg[x] = 0; }
inline void rot(int x) {
    register int y = fa[x], z = fa[y], k = get(x);
    if (nRt(y)) ch[z][get(y)] = x;
    fa[x] = z;
    ch[y][k] = ch[x][k ^ 1], fa[ch[x][k ^ 1]] = y;
    ch[x][k ^ 1] = y, fa[y] = x;
    Up(y), Up(x);
}
inline void splay(int x) {
    register int top, k = x;
    st[top = 1] = k;
    while (nRt(k)) st[++top] = k = fa[k];
    while (top) down(st[top--]);
    for (int y; y = fa[x], nRt(x); rot(x))
        if (nRt(y)) rot(get(x) ^ get(y) ? x : y);
}
inline void access(int x) { for (int y = 0; x; x = fa[y = x]) splay(x), ch[x][1] = y, Up(x); }
inline void make(int x) { access(x), splay(x), rev(x); }
inline void split(int x, int y) { make(x), access(y), splay(y); }
int fd(int x) { return ff[x] == x ? x : ff[x] = fd(ff[x]); }
inline int Find(int x) {
    access(x), splay(x), down(x);
    while (ch[x][0]) down(x = ch[x][0]);
    return splay(x), x;
}
inline void link(int x, int y) {
    make(x);
    if (Find(y) ^ x) fa[x] = y;
}
inline void cut(int x, int y) {
    make(x);
    if (Find(y) == x && fa[y] == x && ch[x][1] == y)
        fa[y] = ch[x][1] = 0, Up(x);
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n + m; i++) ff[i] = id[i] = i;
    for (int i = 1; i <= m; i++) {
        scanf("%d%d%d%d", &e[i].u, &e[i].v, &e[i].a, &e[i].b);
    }
    sort(e + 1, e + m + 1, cmp);
    for (int i = 1; i <= m; i++) val[i + n] = e[i].a;
    for (int i = 1, u, v, p, q, fl, o; i <= m; i++) {
        p = fd(u = e[i].u), q = fd(v = e[i].v), fl = 1;
        if (u == v) continue;
        if (p == q) {
            split(u, v), o = id[v];
            if (val[o] > e[i].a) cut(e[o - n].u, o), cut(o, e[o - n].v);
            else fl = 0;
        } else ff[p] = q;
        if (fl) link(u, i + n), link(i + n, v);
        if (fd(1) == fd(n)) split(1, n), ans = min(ans, e[i].b + val[id[n]]);
    }
    if (ans == 2e9) puts("-1");
    else printf("%d", ans);
}

维护边双

LCT 的一个点表示一个边双。外面用一个并查集维护每个点属于哪一个边拴。

加入边时,如果不联通就 link

否则,形成的环构成一个边双,把环缩成一个点,把并查集也合并。

  • 暴力缩点:将当前辅助树的根节点设置为集合的标志节点, dfs 整个辅助树

    把其他点的并查集祖先修改。并断开标志节点与子树的连接,暴力修改不会超过 \(n\log n\)

[AHOI2005] 航线规划

离线显然,查询其实就是(路径上边双个数减一)。

每用一个点用并查集找到它所在边双,例如 access\(x\leftarrow find(fa_x)\)

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 1e5 + 5;
int n, m, Ti, ty[N], ans[N], outs, dl[N], fa[N], ch[N][2], tg[N], sz[N], st[N], ff[N], qu[N], qv[N];
struct edge {
    int u, v;
    bool operator < (edge A) const {
        if (u ^ A.u) return u < A.u;
        return v < A.v;
    }
} E[N];
int fd(int x) { return ff[x] == x ? x : ff[x] = fd(ff[x]); }
inline void Up(int x) { sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + 1; }
inline int get(int x) { return ch[fd(fa[x])][1] == x; }
inline int nRt(int x) { return ch[fd(fa[x])][0] == x || ch[fd(fa[x])][1] == x; }
inline void rev(int x) { tg[x] ^= 1, swap(ch[x][0], ch[x][1]); }
inline void down(int x) { if (tg[x]) rev(ch[x][0]), rev(ch[x][1]), tg[x] = 0; }
inline void rot(int x) {
    register int y = fd(fa[x]), z = fd(fa[y]), k = get(x);
    if (nRt(y)) ch[z][get(y)] = x;
    fa[x] = z;
    ch[y][k] = ch[x][k ^ 1], fa[ch[x][k ^ 1]] = y;
    ch[x][k ^ 1] = y, fa[y] = x;
    Up(y), Up(x);
}
inline void splay(int x) {
    register int top, k = x;
    st[top = 1] = k;
    while (nRt(k)) st[++top] = k = fd(fa[k]);
    while (top) down(st[top--]);
    for (int y; y = fd(fa[x]), nRt(x); rot(x))
        if (nRt(y)) rot(get(x) ^ get(y) ? x : y);
}
inline void access(int x) { for (int y = 0; x; y = x, x = fd(fa[x])) splay(x), ch[x][1] = y, Up(x); }
inline void make(int x) { access(x), splay(x), rev(x); }
inline int Find(int x) {
    access(x), splay(x), down(x);
    while (ch[x][0]) down(x = ch[x][0]);
    return splay(x), x;
}
inline void split(int x, int y) { make(y), access(x), splay(x); }
void del(int x, int y) {
    if (x) ff[x] = y, sz[y] += sz[x], del(ch[x][0], y), del(ch[x][1], y);
}
inline void mer(int x, int y) {
    if (x == y) return;
    make(x);
    if (Find(y) ^ x) { fa[x] = y; return; }
    del(ch[x][1], x), ch[x][1] = 0, Up(x);
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) sz[i] = 1, ff[i] = i;
    for (int i = 1; i <= m; i++) {
        scanf("%d%d", &E[i].u, &E[i].v);
        if (E[i].u > E[i].v) swap(E[i].u, E[i].v);
    }
    sort(E + 1, E + m + 1);
    for (int op, u, v; scanf("%d", &op), op != -1;) {
        scanf("%d%d", &u, &v);
        if (u > v) swap(u, v);
        ++Ti, qu[Ti] = u, qv[Ti] = v, ty[Ti] = op;
        if (!op) dl[lower_bound(E + 1, E + m + 1, (edge){ u, v }) - E] = 1;
    }
    for (int i = 1; i <= m; i++)
        if (!dl[i]) mer(fd(E[i].u), fd(E[i].v));
    for (int x, y; Ti; --Ti) {
        x = fd(qu[Ti]), y = fd(qv[Ti]);
        if (ty[Ti]) split(x, y), ans[++outs] = sz[x] - 1;
        else mer(x, y);
    }
    while (outs) printf("%d\n", ans[outs--]);
}

维护子树信息

lct 最大痛点,树剖在这里由不可取代的地位。

常见的是维护子树信息总和,如大小、权值。

[BJOI2014]大融合 为例

查询就是 cut(x,y) 后两个子树大小的乘积。

用多一个数组 \(s2\) 记录虚儿子的信息,注意 \(s\) 为所有儿子的信息,包括实儿子

需要修改 pushup, access, link

就是根据虚实边的转化修改。

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
char buf[100000], *S = buf, *T = buf;
inline char G() {
    return S == T && (T = (S = buf) + fread(buf, 1, 100000, stdin), S == T) ? EOF : *S++;
}
inline int Rd() {
    register int x = 0;
    static char C = G();
    for (; C < '0' || C > '9'; C = G()) ;
    for (; C > '/' && C < ':'; C = G()) x = (x << 1) + (x << 3) + (C ^ 48);
    return x;
}
const int N = 100005;
int n, Ti, tg[N], sz[N], sz2[N], fa[N], ch[N][2], st[N], top;
char op;
inline int get(int x) { return ch[fa[x]][1] == x; }
inline int nRt(int x) { return ch[fa[x]][0] == x || ch[fa[x]][1] == x; }
inline void Up(int x) { sz[0] = 0, sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + 1 + sz2[x]; }
inline void rev(int x) { swap(ch[x][0], ch[x][1]), tg[x] ^= 1; }
inline void down(int x) { if (tg[x]) rev(ch[x][0]), rev(ch[x][1]), tg[x] = 0; }
inline void rot(int x) {
    int y = fa[x], z = fa[y], k = get(x);
    if (nRt(y)) ch[z][get(y)] = x;
    fa[x] = z;
    ch[y][k] = ch[x][k ^ 1], fa[ch[x][k ^ 1]] = y;时
    ch[x][k ^ 1] = y, fa[y] = x;
    Up(y), Up(x);
}
inline void upd(int x) {
    st[top = 1] = x;
    while (nRt(x)) st[++top] = x = fa[x];
    while (top) down(st[top--]);
}
inline void splay(int x) {
    upd(x);
    for (int y; y = fa[x], nRt(x); rot(x))
        if (nRt(y)) rot(get(x) ^ get(y) ? x : y);
}
inline void access(int x) {
    for (int y = 0; x; x = fa[y = x])
        splay(x), sz2[x] += sz[ch[x][1]] - sz[y], ch[x][1] = y, Up(x);
}
inline void make(int x) { access(x), splay(x), rev(x); }
inline int Find(int x) {
    access(x), splay(x), down(x);
    while (ch[x][0]) down(x = ch[x][0]);
    return splay(x), x;
}
inline void link(int x, int y) {
    make(x), make(y), fa[x] = y, sz2[y] += sz[x], Up(y);
}
inline void cut(int x, int y) {
    make(x), Find(y), fa[y] = ch[x][1] = 0, Up(x);
}
int main() {
    n = Rd(), Ti = Rd();
    for (int x, y; Ti--; ) {
        op = G();
        while (op < 'A' || op > 'Z') op = G();
        x = Rd(), y = Rd();
        if (op == 'A') link(x, y);
        else {
            cut(x, y), make(x), make(y);
            printf("%lld\n", 1ll * sz[x] * sz[y]);
            link(x, y);
        }
    }
}
posted @ 2022-08-12 19:50  小蒟蒻laf  阅读(158)  评论(0编辑  收藏  举报