月度归档: 2018 年 3 月

Link-Cut Tree(动态树)原理与实现

Link-Cut Tree(动态树)原理与实现

概述

LCT是一种实用数据结构(?),它滋磁维护树上信息,而且这个树是可以变换结构的。具体而言,它维护了一个Splay森林,利用Splay的功能可以维护树上信息,且可以对树结构进行拆分、连接等操作。下面介绍LCT的原理与实现。

原理与实现

一些定义

辅助树(auxiliary tree):在LCT中,用于维护重链信息的Splay。其维护的有序性以深度为参数,即其中序遍历为该链从根到重儿子的顺序。
访问(access):从树根出发,将根到某个节点的路径设置为重链。
重儿子(偏好儿子,preferred child):访问到的树链的最底层节点。
重边(偏好边,preferred edge):重链中,指向重儿子方向的边。
重链(偏好链,preferred path):访问时走过的路径,由重边和重儿子组成。
所有的访问都会创造出重链,但是由于LCT的操作,重链可能会断掉。具体的操作需要等到讲到操作的时候才能够说清楚。
注意,辅助树是为了便于维护树上信息,其形态不一定与原树相同,我们后面会讲到辅助树与原树的关系。

树的结构

刚刚也说到了,实际上LCT维护的是一个Splay森林。一棵Splay维护的是一条重链的信息。下面是一个LCT的示意图。
180317a 1 LCT intro - Link-Cut Tree(动态树)原理与实现
图片来自Wikipedia:Link/cut tree – Wikipedia
图片中,粗边即为重边,而细边我们将它定义为轻边。左→中表示的是以a为根的有根树访问l节点所造成的变化。右图展示的是Splay的划分。
我们说,Splay维护的是重链信息,而当前Splay和原树中重链顶的父节点通过轻边(右图虚线箭头)来连接。
从现在开始,文章内容默认你已经会使用Splay的操作。如果Splay仍不会,可以参考Splay原理与实现 | KSkun’s Blog一文。

LCT的核心:访问(access)

访问,即从树根出发,将树根到某节点x的路径设置为重链的操作。要实现这个操作,我们需要将这一排点加入到一个Splay中。
实现上,我们要断掉链上的点与之前重儿子的连接,将其合并到一个Splay中。我们从重儿子x向树根处理,每次将节点splay到所在辅助树的根,此时该点的右子树为以前重链上需要切掉的一段。切断这条重边,并将右儿子的连边设置为向当前重儿子的重边即可。
实现上,我们统一用fa这一变量来存储父亲的信息,也就是说这个fa可能指的并非所在Splay的父节点,而是轻边的父节点。但是每个节点的儿子信息则完全是Splay中的儿子信息,即轻边的儿子不会在父节点有记录。因此我们只需更改每个节点的右儿子即可,并不需要重置右儿子的父亲信息。

inline void access(int p) {
    for(int q = 0; p; q = p, p = tr[p].fa) {
        splay(p);
        tr[p].ch[1] = q;
        update(p);
    }
}

换根(make_root)

换根,即将原树转换为以某节点x为树根的有根树。
我们可以访问该节点,并将其splay至根。由于Splay内部的有序以深度为依据,x只有左儿子,此时表示x在重链的底端。我们对这棵Splay进行翻转,x就变为重链的顶端,即原树树根。

inline void makert(int p) {
    access(p);
    splay(p);
    reverse(p);
}

找根(find_root)

找根,即找到某节点x所在原树的树根。
先访问该节点,splay至根。此时这条链上深度最小的点即为原树树根。因此我们只需找到最左的左儿子即可。

inline int findrt(int p) {
    access(p);
    splay(p);
    while(tr[p].ch[0]) p = tr[p].ch[0];
    return p;
}

分离树链(split)

分离树链,即将原树中某节点u到v的路径分离出来。
我们考虑将链的一端u设为树根,并访问v,将v splay到根。此时对于这条树链,在u、v所在的Splay上的体现即为v及v的左子树。

inline void split(int u, int v) {
    makert(u);
    access(v);
    splay(v);
}

连接原树(link)

连接原树,即将某节点u、v所在的原树通过边(u, v)连接起来。
先将其中一点u设为原树根,以轻边接到v上即可。如果此处利用split,效果其实是一样的。

inline void link(int u, int v) {
    split(u, v);
    tr[u].fa = v;
}

切断原树(cut)

切断原树,即将本相连的节点u、v之间的边切断。
先将这条边split出来,然后切断父子关系即可。如果左儿子不是u或者左儿子有右儿子,那么表示不存在(u, v)这条边。

inline void cut(int u, int v) {
    split(u, v);
    if(tr[v].ch[0] != u || tr[tr[v].ch[0]].ch[1]) return;
    tr[u].fa = tr[v].ch[0] = 0;
}

改变点值(modify)

你需要把该点提至整棵树的树根,再做修改,这样可以避免修改后还要向上更新根的信息。

inline void modify(int u, int w) {
    access(u);
    splay(u);
    tr[u].val = w; // 更改值
    update(u);
}

与单独Splay操作的区别

由于我们用fa混着存两种边关系,即轻儿子、重儿子,我们不好判断Splay根的位置。下面是一种判断是否为Splay根的方法。

inline bool isroot(int p) {
    int fa = tr[p].fa;
    return tr[fa].ch[0] != p && tr[fa].ch[1] != p;
}

对于Splay中常见的旋转(rotate)、伸展(splay)操作,实现上有些许区别。主要原因判断Splay根的方式变了。

inline void rotate(int p) {
    bool t = !isleft(p); int fa = tr[p].fa, ffa = tr[fa].fa;
    tr[p].fa = ffa; if(!isroot(fa)) tr[ffa].ch[!isleft(fa)] = p;
    tr[fa].ch[t] = tr[p].ch[!t]; tr[tr[fa].ch[t]].fa = fa;
    tr[p].ch[!t] = fa; tr[fa].fa = p;
    update(fa);
}

inline void splay(int p) {
    pushto(p);
    for(int fa = tr[p].fa; !isroot(p); rotate(p), fa = tr[p].fa) {
        if(!isroot(fa)) rotate(isleft(fa) == isleft(p) ? fa : p);
    }
    update(p); // 对p只update一次,优化常数
}

辅助树与原树的关系

原树在LCT中其实是剖成几条树链的,每条树链一个Splay。原树的结构中树链顺序,则为Splay的顺序,父子关系也依赖着轻重边来进行管理。但是LCT这个奇妙的Splay森林结构并不和原来的有根树完全相同,Splay根和树根也并不相同。
最开始看LCT就是在这里搞不懂的。而且直到现在我也不知道应该怎么写这段内容GG。

例题:【P3690】【模板】Link Cut Tree (动态树) – 洛谷

题目描述

  1. 求链点权异或和
  2. 连接树边
  3. 断开树边
  4. 改变点权

题解

我们在节点处维护一个子树异或和(Splay意义上的子树),每次查询split以后查。
修改操作则先访问它并且把这个点splay到根再改。

代码

// Code by KSkun, 2018/4
#include <cstdio>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 300005;

struct Node {
    int ch[2], fa, val, xsum;
    bool rev;
} tr[MAXN];

inline bool isleft(int p) {
    return tr[tr[p].fa].ch[0] == p;
}

inline bool isroot(int p) {
    return tr[tr[p].fa].ch[0] != p && tr[tr[p].fa].ch[1] != p;
}

inline void update(int p) {
    tr[p].xsum = tr[p].val ^ tr[tr[p].ch[0]].xsum ^ tr[tr[p].ch[1]].xsum;
}

inline void reverse(int p) {
    std::swap(tr[p].ch[0], tr[p].ch[1]);
    tr[p].rev ^= 1;
}

inline void pushdown(int p) {
    if(tr[p].rev) {
        if(tr[p].ch[0]) reverse(tr[p].ch[0]);
        if(tr[p].ch[1]) reverse(tr[p].ch[1]);
        tr[p].rev ^= 1;
    }
}

int sta[MAXN], stop;

inline void pushto(int p) {
    stop = 0;
    while(!isroot(p)) {
        sta[stop++] = p;
        p = tr[p].fa;
    }
    sta[stop++] = p;
    while(stop) {
        pushdown(sta[--stop]);
    }
}

inline void rotate(int p) {
    bool t = !isleft(p); int fa = tr[p].fa, ffa = tr[fa].fa;
    tr[p].fa = ffa; if(!isroot(fa)) tr[ffa].ch[!isleft(fa)] = p;
    tr[fa].ch[t] = tr[p].ch[!t]; tr[tr[fa].ch[t]].fa = fa;
    tr[p].ch[!t] = fa; tr[fa].fa = p;
    update(fa);
}

inline void splay(int p) {
    pushto(p);
    for(int fa = tr[p].fa; !isroot(p); rotate(p), fa = tr[p].fa) {
        if(!isroot(fa)) rotate(isleft(fa) == isleft(p) ? fa : p);
    }
    update(p);
}

inline void access(int p) {
    for(int q = 0; p; q = p, p = tr[p].fa) {
        splay(p);
        tr[p].ch[1] = q;
        update(p);
    }
}

inline void makert(int p) {
    access(p);
    splay(p);
    reverse(p);
}

inline int findrt(int p) {
    access(p);
    splay(p);
    while(tr[p].ch[0]) p = tr[p].ch[0];
    return p;
}

inline void split(int u, int v) {
    makert(u);
    access(v);
    splay(v);
}

inline void link(int u, int v) {
    split(u, v);
    tr[u].fa = v;
}

inline void cut(int u, int v) {
    split(u, v);
    if(tr[v].ch[0] != u || tr[u].ch[1]) return;
    tr[u].fa = tr[v].ch[0] = 0;
}

inline void modify(int u, int w) {
    access(u);
    splay(u);
    tr[u].val = w;
    update(u);
}

int n, m, op, x, y;

int main() {
    n = readint(); m = readint();
    for(int i = 1; i <= n; i++) {
        tr[i].val = tr[i].xsum = readint();
    }
    while(m--) {
        op = readint(); x = readint(); y = readint();
        switch(op) {
        case 0:
            split(x, y);
            printf("%d\n", tr[y].xsum);
            break;
        case 1:
            link(x, y);
            break;
        case 2:
            cut(x, y);
            break;
        case 3:
            modify(x, y);
        }
    }
    return 0;
}

参考资料

[国家集训队]聪聪可可 题解

[国家集训队]聪聪可可 题解

题目地址:洛谷:【P2634】[国家集训队]聪聪可可 – 洛谷、BZOJ:Problem 2152. — 聪聪可可

题目描述

聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。
他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。
聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。

输入输出格式

输入格式:
输入的第1行包含1个正整数n。后面n-1行,每行3个整数x、y、w,表示x号点和y号点之间有一条边,上面的数是w。

输出格式:
以即约分数形式输出这个概率(即“a/b”的形式,其中a和b必须互质。如果概率为1,输出“1/1”)。

输入输出样例

输入样例#1:

5
1 2 1
1 3 2
1 4 1
2 5 3

输出样例#1:

13/25

题解

直接把距离、边权对3取模存起来,这样可以减少模运算的次数,优化常数。
统计一个子树下点的距离对3取余为0、1、2的点有多少,答案即c_0^2 + 2c_1c_2(距离被3整除的点之间以及和根,距离余3为1、2的点互相构成点对)。然后把不合法方案减掉,套个gcd,就解决了。

代码

// Code by KSkun, 2018/3
#include <cstdio>

inline int max(int a, int b) {
    return a > b ? a : b;
} 

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline int readint() {
    register int res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 20005, INF = 1e9;

int n, m, ut, vt, wt, k, rt, res = 0;
int siz[MAXN], dep[MAXN], msz[MAXN], sum;
bool vis[MAXN];

struct Edge {
    int to, w, nxt;
} gra[MAXN << 1];
int head[MAXN], tot = 0;

inline void addedge(int u, int v, int w) {
    tot++;
    gra[tot].to = v;
    gra[tot].w = w;
    gra[tot].nxt = head[u];
    head[u] = tot;
}

int c[3];

inline void getroot(int u, int fa) {
    siz[u] = 1;
    msz[u] = 0; 
    for(int i = head[u]; i; i = gra[i].nxt) {
        int v = gra[i].to;
        if(vis[v] || v == fa) continue;
        getroot(v, u);
        siz[u] += siz[v];
        msz[u] = max(msz[u], siz[v]);
    }
    msz[u] = max(msz[u], sum - siz[u]);
    if(msz[u] < msz[rt]) rt = u;
}

inline void caldep(int u, int fa) {
    c[dep[u]]++;
    for(int i = head[u]; i; i = gra[i].nxt) {
        int v = gra[i].to, w = gra[i].w;
        if(vis[v] || v == fa) continue;
        dep[v] = (dep[u] + w) % 3;
        caldep(v, u); 
    }
}

inline int work(int u, int w) {
    c[0] = c[1] = c[2] = 0;
    dep[u] = w;
    caldep(u, 0);
    return c[0] * c[0] + c[1] * c[2] * 2;
}

inline void dfs(int u) {
    res += work(u, 0);
    vis[u] = true;
    for(int i = head[u]; i; i = gra[i].nxt) {
        int v = gra[i].to, w = gra[i].w;
        if(vis[v]) continue;
        res -= work(v, w);
        rt = 0;
        sum = siz[v];
        getroot(v, 0);
        dfs(rt);
    }
}

inline int gcd(int a, int b) {
    int t;
    while(t = a % b) {
        a = b; b = t;
    }
    return b;
}

int main() {
    n = readint();
    for(int i = 1; i < n; i++) {
        ut = readint(); vt = readint(); wt = readint() % 3;
        addedge(ut, vt, wt);
        addedge(vt, ut, wt);
    }
    rt = 0;
    msz[0] = INF;
    sum = n;
    getroot(1, 0);
    dfs(rt);
    int t1 = n * n, t2 = gcd(res, t1);
    printf("%d/%d", res / t2, t1 / t2);
    return 0;
}
[BZOJ3589]动态树 题解

[BZOJ3589]动态树 题解

题目地址:BZOJ:Problem 3589. — 动态树

题目描述

小明在楼下种了一棵动态树, 该树每天会在某些节点上长出一些果子. 这棵树的根节点为1, 它有n个节点, n-1条边.
别忘了这是一棵动态树, 每时每刻都是动态的. 小明要求你在这棵树上维护两种事件
事件0:
这棵树长出了一些果子, 即某个子树中的每个节点都会长出K个果子.
事件1:
小明希望你求出几条树枝上的果子数. 一条树枝其实就是一个从某个节点到根的路径的一段. 每次小明会选定一些树枝, 让你求出在这些树枝上的节点的果子数的和. 注意, 树枝之间可能会重合, 这时重合的部分的节点的果子只要算一次.
初始时, 每个节点上都没有果子.

输入输出格式

输入格式:
第一行一个整数n(1<=n<=200,000), 即节点数.
接下来n-1行, 每行两个数字u, v. 表示果子u和果子v之间有一条直接的边. 节点从1开始编号.
在接下来一个整数nQ(1<=nQ<=200,000), 表示事件.
最后nQ行, 每行开头要么是0, 要么是1.
如果是0, 表示这个事件是事件0. 这行接下来的2个整数u, delta表示以u为根的子树中的每个节点长出了delta个果子.
如果是1, 表示这个事件是事件1. 这行接下来一个整数K(1<=K<=5), 表示这次询问涉及K个树枝. 接下来K对整数u_k, v_k, 每个树枝从节点u_k到节点v_k. 由于果子数可能非常多, 请输出这个数模2^31的结果.

输出格式:
对于每个事件1, 输出询问的果子数.

输入输出样例

输入样例#1:

5
1 2
2 3
2 4
1 5
3
0 1 1
0 2 3
1 2 3 1 1 4

输出样例#1:

13

说明

1 <= n <= 200,000, 1 <= nQ <= 200,000, K = 5.
生成每个树枝的过程是这样的:先在树中随机找一个节点, 然后在这个节点到根的路径上随机选一个节点, 这两个节点就作为树枝的两端.

题解

又是树链信息,套树剖。考虑线段树维护区间和,然后使用打标记的形式来处理链并的问题。链上的点都打上标记,只有打标记的点的权值才会计入答案,这个线段树写是容易的。记得每次清空这次打过的标记。
至于那个取模,模数是2147483648,我们可以用int自然溢出以后对2147483647取与,这样就能把符号位搞掉,得到取模后的答案,减小常数。
吐槽:不要用memset处理很大的数组,会TLE。线段树做是O(\log n)比memsetO(n)优。

代码

// Code by KSkun, 2018/3
#include <cstdio>
#include <cstring>

#include <vector>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 200005;

int n, q, ut, vt, op, xt, kt;
int fa[MAXN], siz[MAXN], son[MAXN], dfn[MAXN], ptn[MAXN], top[MAXN], dep[MAXN], cnt;
std::vector<int> gra[MAXN];

inline void addedge(int u, int v) {
    gra[u].push_back(v);
    gra[v].push_back(u);
}

inline void dfs1(int u) {
    for(int i = 0; i < gra[u].size(); i++) {
        int v = gra[u][i];
        if(v == fa[u]) continue;
        dep[v] = dep[u] + 1;
        fa[v] = u;
        dfs1(v);
        siz[u] += siz[v] + 1;
        if(siz[v] > siz[son[u]]) son[u] = v;
    }
}

inline void dfs2(int u, int tp) {
    top[u] = tp;
    dfn[u] = ++cnt;
    ptn[dfn[u]] = u;
    if(son[u]) dfs2(son[u], tp);
    for(int i = 0; i < gra[u].size(); i++) {
        int v = gra[u][i];
        if(v == son[u] || v == fa[u]) continue;
        dfs2(v, v);
    }
}

int val[MAXN << 2], sum[MAXN << 2], add[MAXN << 2], tag[MAXN << 2];

inline void pushdown(int o, int l, int r) {
    int mid = (l + r) >> 1, lch = o << 1, rch = (o << 1) | 1;
    if(add[o]) {
        add[lch] += add[o];
        add[rch] += add[o];
        val[lch] += add[o] * (mid - l + 1);
        val[rch] += add[o] * (r - mid);
        add[o] = 0;
    }
    if(tag[o] != -1) {
        tag[lch] = tag[rch] = tag[o];
        sum[lch] = val[lch] * tag[o];
        sum[rch] = val[rch] * tag[o];
        tag[o] = -1;
    }
}

inline void modify(int o, int l, int r, int ll, int rr, int v) {
    if(l >= ll && r <= rr) {
        val[o] += v * (r - l + 1);
        add[o] += v;
        return;
    }
    pushdown(o, l, r);
    int mid = (l + r) >> 1, lch = o << 1, rch = (o << 1) | 1;
    if(ll <= mid) modify(lch, l, mid, ll, rr, v);
    if(rr > mid) modify(rch, mid + 1, r, ll, rr, v);
    val[o] = val[lch] + val[rch];
}

inline void select(int o, int l, int r, int ll, int rr, int v) {
    if(l >= ll && r <= rr) {
        sum[o] = val[o] * v;
        tag[o] = v;
        return;
    }
    pushdown(o, l, r);
    int mid = (l + r) >> 1, lch = o << 1, rch = (o << 1) | 1;
    if(ll <= mid) select(lch, l, mid, ll, rr, v);
    if(rr > mid) select(rch, mid + 1, r, ll, rr, v);
    sum[o] = sum[lch] + sum[rch];
}

inline void modify(int u, int v, int x) {
    int tu = top[u], tv = top[v];
    while(tu != tv) {
        if(dep[tu] > dep[tv]) {
            std::swap(u, v);
            std::swap(tu, tv);
        }
        modify(1, 1, n, dfn[tv], dfn[v], x);
        v = fa[tv];
        tv = top[v];
    }
    if(dep[u] > dep[v]) std::swap(u, v);
    modify(1, 1, n, dfn[u], dfn[v], x);
}

inline void select(int u, int v) {
    int tu = top[u], tv = top[v];
    while(tu != tv) {
        if(dep[tu] > dep[tv]) {
            std::swap(u, v);
            std::swap(tu, tv);
        }
        select(1, 1, n, dfn[tv], dfn[v], 1);
        v = fa[tv];
        tv = top[v];
    }
    if(dep[u] > dep[v]) std::swap(u, v);
    select(1, 1, n, dfn[u], dfn[v], 1);
}

int main() {
    n = readint();
    for(int i = 1; i < n; i++) {
        ut = readint(); vt = readint();
        addedge(ut, vt);
    }
    dfs1(1);
    dfs2(1, 1);
    q = readint();
    while(q--) {
        op = readint();
        if(op == 0) {
            ut = readint(); xt = readint();
            modify(1, 1, n, dfn[ut], dfn[ut] + siz[ut], xt);
        } else {
            kt = readint();
            while(kt--) {
                ut = readint(); vt = readint();
                select(ut, vt);
            }
            printf("%d\n", sum[1] & 2147483647);
            select(1, 1, n, 1, n, 0);
        }
    }
    return 0;
}