定义

树的直径是指:在一棵有权/无权树中,所有简单路径中,权值和最大的那一条

树的直径有以下性质:(以下,我们假设所有边上的权值均 $\geq 0$)。

  1. 直径一定是由两个 leaf 组成

  2. 对于任意一个节点 $u$,距离它最远的一个节点,必然为直径的其中一端。

  3. 在一棵树上,任取两个点集 $S_1, S_2$,设 $S_1$ 这个点集的直径是 $(u_1,v_1)$,设 $S_2$ 这个点集的直径是 $(u_2, v_2)$,那么令 $S = S_1 \cup S_2$。则点集 $S$ 的直径只可能为以下 $6$ 种的之一:

    $$(u_1, v_1), (u_2, v_2), (u_1, u_2), (u_1, v_2), (v_1, u_2), (v_2, u_2)$$

    即,新的直径一定由原先 $4$ 个端点组成。

证明:第二个性质

我们设直径是 $(s,t)$,并且设节点 $a$ 距离最远的节点是 $b$,其中 $b \neq s, b \neq t$。

Case1:$a$ 位于直径 $(s,t)$ 上:

img

如上,有 $L_1 > L_2$,$L_1 > L_3$。且 $L_2 + L_3$ 为直径。

那么 $L_1 + L_3 > L_2 + L_3$,$L_1 + L_2 > L_2 + L_3$。

所以 $L_1 + L_3$ 或者 $L_1 + L_2$ 才是直径。contradiction。


Case2:$(a,b)$ 之间的路径横穿了 $(s,t)$,交点为 $x$。

img

如上,因为距离 $a$ 最远的是 $b$,所以有 $L_1 + L_2 > L_1 + L_3$,$L_1 + L_2 > L_1 + L_3$。

那么 $L_2 > L_3$,$L_2 > L_4$。

所以 $L_2 + L_3 > L_3 + L_4$,$L_2 + L_4 > L_3 + L_4$。

所以 $L_2 + L_3$ 或者 $L_2 + L_4$ 才是直径。contradiction。


Case3:$(a,b)$ 之间的路径没有穿过 $(s,t)$,但是 $(a,b)$ 路径上,有一个距离 $(s,t)$ 最短的点 $x$,并且这个 $x$ 通过 $y$ 与 $(s,t)$ 相交。

img

如上,因为距离 $a$ 最远的是 $b$,所以有 $L_1 + L_2 > L_1 + L_5 + L_4$,$L_1 + L_2 > L_1 + L_5 + L_3$。

所以有 $L_2 > L_5 + L_4$,$L_2 > L_5 + L_3$。

所以 $L_3 + L_5 + L_2 > L_3 + L_4$,$L_4 + L_5 + L_2 > L_3 + L_4$。

所以 $L_3 + L_5 + L_2$ 或者 $L_4 + L_5 + L_2$ 才是直径。contradiction。

• 第三个性质的证明不会,先咕着

求树的直径

求树的直径有两种方法:两次DFS 和 DP。

法一:两次DFS(推荐)

由性质 $2$,我们可以以任意节点为根,进行DFS。得到一个距离最远的点 $u$ (这个节点的深度最深)。

然后再以 $u$ 为根,进行第二次DFS。得到距离最远的点 $v$。那么 $(u,v)$ 就是直径了。

两次DFS:代码
int n, dep[maxn], d1, d2;  // d1: 直径一端,d2: 直径另外一端
void dfs1(int u, int p) {
    for (int e = head[u]; e; e = edges[e].nxt) {
        int to = edges[e].to;
        if (to == p) continue;
        dep[to] = dep[u] + 1;
        dfs1(to, u);
    }
}
int main() {
    dep[1] = 0; dfs1(1, 0);  // 第一次 DFS (以 1 为根)
 
    d1 = 1;
    for (int u = 1; u <= n; u++) {
        if (dep[u] > dep[d1]) {
            d1 = u;
        }
    }
    dep[d1] = 0; dfs1(d1, 0);  // 第二次 DFS (以直径端点 d1 为根)
 
    d2 = d1;
    for (int u = 1; u <= n; u++) {
        if (dep[u] > dep[d2]) {
            d2 = u;
        }
    }
}

法二:树形DP

固定 $1$ 为根。记录每个节点向下,最远能延伸的两个节点的距离 $d_1,d_2$,那么直径就是所有 $d_1+d_2$ 的最大值。

树形DP:代码
int d1[maxn], d2[maxn], d;  // d 是直径的值
void dfs(int u, int p) {
    d1[u] = d2[u] = 0;
    for (int e = head[u]; e; e = edges[e].nxt) {
        int to = edges[e].to;
        if (to == p) continue;
        dfs(to, u);
        int t = d1[v] + 1;
        if (t > d1[u])
        d2[u] = d1[u], d1[u] = t;
        else if (t > d2[u])
        d2[u] = t;
    }
    d = max(d, d1[u] + d2[u]);
}

int main() {
    dfs(1, 0);
}

例题

例1 CF911F Tree Destruction

题意

给定一棵 $n$ 个节点的无权树,然后进行 $(n-1)$ 次以下操作,每次操作分以下三步:

  1. 选择两个leaf
  2. 将这两个leaf之间的距离,加到 ans 中。
  3. 将这两个leaf其中之一,删掉。

初始状态下,ans = 0。求 ans 的最大值,并且输出方案。

其中,$2 \leq n \leq 2 \times 10^5$

题解

利用树的直径的第二个性质:任意一个节点 $u$,距离最远的一定是直径的一端

所以我们要最大化最终答案,我们可以最大化每个节点的贡献。所以我们只要保留直径,先将剩下的叶子删掉就行了。

思路如下:

  1. 找到直径 $(u,v)$
  2. 使用类似于拓扑排序的方法,维护所有 deg = 1 的节点(除了直径两端的两个节点 $u,v$),将它们删去。这个过程,本质上是将直径看作树干,然后逐一拔掉所有的树枝。
  3. 最后只剩下一条直径了,从任意一端开始删除即可。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5+5; 
 
struct Edge {
    int to, nxt;
} edges[maxn<<1];
int head[maxn], ecnt = 1, n, dep[maxn], d1, d2;  // d1: 直径一端,d2: 直径另外一端
 
void addEdge(int u, int v) {
    Edge e = {v, head[u]};
    edges[ecnt] = e;
    head[u] = ecnt++;
}
 
void dfs1(int u, int p) {
    for (int e = head[u]; e; e = edges[e].nxt) {
        int to = edges[e].to;
        if (to == p) continue;
        dep[to] = dep[u] + 1;
        dfs1(to, u);
    }
}
 
ll ans = 0;
struct node {
    int u,v,r;
} path[maxn];
int tail = 0;
 
int deg[maxn];
vector<int> leaf;
 
int dis1[maxn], dis2[maxn];
// 初始化:找出每个节点到 d1, d2的距离
void init() {
    dep[d1] = 0; dfs1(d1, 0);
    memcpy(dis1, dep, sizeof(dep));
    dep[d2] = 0; dfs1(d2, 0);
    memcpy(dis2, dep, sizeof(dep));
}
 
// 第一步:拔掉所有除了 d1, d2 以外的叶子
void solve1() {
    for (int i = 1; i <= n; i++) {
        if (deg[i] == 1 && i != d1 && i != d2) leaf.push_back(i);
    }
    while (leaf.size()) {
        int u = leaf.back(); leaf.pop_back();
        for (int e = head[u]; e; e = edges[e].nxt) {
            int to = edges[e].to;
            deg[to]--;
            if (deg[to] == 1) {
                leaf.push_back(to);
            }
        }
        int t;
        if (dis1[u] > dis2[u]) t = d1, ans += (ll)dis1[u];
        else t = d2, ans += (ll)dis2[u];
        path[++tail] = {u,t,u};
    }
}
 
// 第二步:拔掉直径
void solve2() {
    leaf.push_back(d2);
    while (leaf.size()) {
        int u = leaf.back(); leaf.pop_back();
        for (int e = head[u]; e; e = edges[e].nxt) {
            int to = edges[e].to;
            deg[to]--;
            if (deg[to] == 1) {
                leaf.push_back(to);
            }
        }
        ans += (ll)(dis1[u]);
        path[++tail] = {u, d1, u};
    }
}
  
int main() {
    cin >> n;
    for (int i = 1; i <= n-1; i++) {
        int u,v; cin >> u >> v;
        addEdge(u,v); addEdge(v,u);
        deg[u]++, deg[v]++;
    }
    dep[1] = 0; dfs1(1, 0);
 
    d1 = 1;
    for (int u = 1; u <= n; u++) {
        if (dep[u] > dep[d1]) {
            d1 = u;
        }
    }
 
    dep[d1] = 0; dfs1(d1, 0);
 
    d2 = d1;
    for (int u = 1; u <= n; u++) {
        if (dep[u] > dep[d2]) {
            d2 = u;
        }
    }
 
    init();
    solve1();
    solve2();
 
    cout << ans << endl;
    for (int i = 1; i <= n-1; i++) {
        cout << path[i].u << " " << path[i].v << " " << path[i].r << "\n";
    }
}

例2 CF1192B Dynamic Diameter

题意

给定 $n$ 个节点的有权树,和 $q$ 次询问。

每次询问格式为 $d ~ e$:将第 $d$ 条边的权值改为 $e$。

在每次询问之后,回答:此时树中直径的长度。

其中,$2 \leq n \leq 10^5, 1 \leq q \leq 10^5$。

欧拉序介绍

欧拉序和DFS序不同的地方在于:

在一个节点 $u$ 访问完它的一个child $v$ 之后,回溯到 $u$ 时,也会将 $u$ 记录进序列中

img

如上图,访问的顺序如果是 $1 \rightarrow 2 \rightarrow 4 \rightarrow 5 \rightarrow 6 \rightarrow 3$ 的话,得到的欧拉序就是:

$[1,2,4,5,4,2,6,2,1,3,1]$。

欧拉序有以下性质:

  1. 对于 $n$ 个节点的树,欧拉序的长度一定为 $2n-1$。
  2. 在欧拉序中任取两个index $i,j$,它分别对应的节点是 $u,v$ 的话。在 $[i,j]$ 这段欧拉序中,depth 最小的index 所对应的节点 $x$ 就是 $LCA(u,v)$。

• 例如,我们把上述欧拉序按照 depth 重新写一下,就会得到:[0,1,2,3,2,1,2,1,0,1,0]

我们任取其中一段,比如:[0,1,2,3,2,1,2,1,0,1,0],那么对应的就是 $5,4,2,6$ 这几个节点。可以发现这一段中,depth 最小值为 $1$,对应的节点是 $2$。而这一段的两端,对应的就是节点 $5,6$。这说明 节点 $5,6$ 的 LCA 就是 节点 $2$。


证明性质 $1$:易知,每条 edge 会在欧拉序中贡献 $2$ 个位置。而根节点 $1$ 又会额外贡献一个。所以总共是 $2 \times (n-1) + 1 = 2n-1$。

证明性质 $2$:设一段欧拉序的两端,对应的节点分别为 $u,v$。令 $x = LCA(u,v)$。那么这一段欧拉序,一定会经过 $x$,且一定是完全在 $x$ 所在的子树内的,不可能出现在子树外的节点。所以 $x$ 一定是这一段 depth 最小的那个节点。

法一 欧拉序 题解

有两种方法,一种是 欧拉序,复杂度 $O(n\log n)$。还有一种是 DFS序,时间复杂度 $O(n\log^2 n)$。

这里介绍一下 法一欧拉序。


我们固定 $1$ 为根。求出每个节点的 depth。然后求出欧拉序。

有了欧拉序以后,我们可以用线段树维护这个欧拉序序列。我们设这个序列为 $a[]$。

然后询问两个节点 $u,v$ 之间的距离,就可以先找到 $u$ 对应在欧拉序中的位置 $l$(可能对应多个位置,任选一个即可),$v$ 对应在欧拉序中的位置 $r$。由树上差分,可知 $u,v$ 的距离等于 $d_u + d_v - 2 \times d_x$,则有:

$$dist(u,v) = a[l] + a[r] - 2 \times \min\limits_{l \leq k \leq r}a[k]$$


所以整个问题可以转化为一个序列上的问题:给定一个数组 $a[]$,求:

$$a[l] + a[r] - 2 \times \min\limits_{l \leq k \leq r}a[k]$$

最大值?(其中 $l,r$ 任选)

这个式子有 $2$ 个变量,$l,r$,我们需要用线段树来维护一些额外信息,来将其转化为一个变量的问题。

$$D[L,R] = \max\limits_{l \leq r, l,r \in [L,R]} \{a[l] + a[r] - 2 \times \min\limits_{l \leq k \leq r}a[k]\}$$

我们考虑固定其中一个变量(比如 $l$),那么剩下的变量就只有 $r$ 了,我们就可以考虑 $a[r] - 2 \times \min\limits_{l \leq k \leq r}a[k]$。

$$rmax[L,R] = \max\limits_{l \leq r, l,r \in [L,R]} \{a[r] - 2 \times \min\limits_{l \leq k \leq r}a[k] \}$$

$$lmax[L,R] = \max\limits_{l \leq r, l,r \in [L,R]} \{a[l] - 2 \times \min\limits_{l \leq k \leq r}a[k] \}$$

则,我们有以下转移方程:

$$rmax[L,R] = \max \begin{cases} rmax[L, mid] \\
rmax[mid+1, R] \\
max[mid+1,R] - 2\times min[L, mid] \end{cases} $$

$$lmax[L,R] = \max \begin{cases} lmax[L, mid] \\
lmax[mid+1, R] \\
max[L, mid] - 2\times min[mid+1, R] \end{cases} $$

$$D[L,R] = \max \begin{cases} D[L, mid] \\
D[mid+1, R] \\
max[L, mid] + rmax[mid+1, R] \\
lmax[L, mid] + max[mid+1, R] \end{cases} $$

最后,答案就是 $D[1,2n-1]$。


上述转移方程有几个点需要注意:

Q1. 为什么 $D[L,R]$ 可以由 $max[L, mid] + rmax[mid+1, R]$ 转移而来?$rmax[mid+1, R]$ 并没有考虑到 $[L,mid]$ 这一段中的最小值啊?

A1. 如果最终答案需要考虑到 $[L,mid]$ 这一段的最小值,那么说明 $D[L,R]$ 应该由 $lmax[L, mid] + max[mid+1, R]$ 或者 $D[L, mid]$ 转移而来。这也说明了为什么我们需要使用 $lmax, rmax$ 进行两次转移。

• 另外,$rmax[L,R]$ 和 $lmax[L,R]$ 的转移过程同理。


最后一个问题:修改边的权值,怎么处理?

因为我们所有的距离都是基于 depth 的,所以,把修改边的权值改为修改 depth 即可。

假设有一个边 $(u,v)$,令 $v$ 为 child。设原来的权值为 $e_1$,现在改为 $e_2$,那么就相当于给 $v$ 的 subtree 中的所有节点的 depth 加上 $(e_2 - e_1)$。

法二 DFS序 题解

根据树的直径的性质 $3$,我们可以求出整棵树的欧拉序,然后用线段树维护点集的直径。比如 $[5,8]$ 这个区间,维护的就是欧拉序为 $[5,6,7,8]$ 的点集的直径。

可得,$[1,n]$ 这个点集的直径就是整棵树的直径。

然后在合并区间 $[L, mid]$ 和 $[mid+1, R]$ 时,就可以枚举 $6$ 种情况的直径长度,得到 $[L,R]$ 对应点集的直径。


有几个问题:

Q1. 怎么得到每种情况的直径长度?

A1. 和上一个方法一样,固定 $1$ 为根,然后两个节点 $(u,v)$ 之间的距离就可以利用 $d_u + d_v - 2 \times d_{LCA(u,v)}$ 来求出。

Q2. 修改权值时,直径会受到影响吗?

A2. 会的。但是如果我们修改了权值,影响了一个 subtree(在DFS序上就是一段连续区间),则如果一个点集完全存在于这个 subtree 之内,就不会受到影响。我们只需要考虑 点集 与 subtree 拥有相交区间,且点集没有被完全覆盖的情况。这实际上就是线段树 update 过程中正常的 lazy tag 和 push_up() 操作。所以不需要特殊处理,该怎么写怎么写就行。


注:本题没有用法二来写,因为本题不需要求出直径具体的节点,只要长度。下一题会使用到法二,代码可以参见下一个例题。

法一 欧拉序 代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;
const int maxm = 1e6;

struct Edge {
    int from, to, nxt;
    ll w;
} edges[maxn<<1];
int head[maxn], ecnt = 2, n, q;
ll W;
void addEdge(int u, int v, ll w) {
    Edge e = {u, v, head[u], w};
    edges[ecnt] = e;
    head[u] = ecnt++;
}

ll dep[maxn<<1];  // 欧拉序的 dep
int in[maxn], out[maxn];  // in[u]: 节点 u 在欧拉序中的起点
int ori_dep[maxn];  // 原本在树中的depth(不算weight)
int id = 0;

void dfs(int u, int p, ll d) {
    dep[++id] = d;
    in[u] = out[u] = id;
    for (int e = head[u]; e; e = edges[e].nxt) {
        int to = edges[e].to;
        if (to == p) continue;
        ll w = edges[e].w;
        ori_dep[to] = ori_dep[u] + 1;
        dfs(to, u, d + w);
        dep[++id] = d;
        out[u] = id;
    }
}

int m;
struct tree_node {
    ll mini, maxi, lmax, rmax, ans;
    ll lazy = 0;
} tr[maxn<<3];

void push_up(int cur) {
    int l = cur<<1, r = l+1;
    tr[cur].mini = min(tr[l].mini, tr[r].mini);
    tr[cur].maxi = max(tr[l].maxi, tr[r].maxi);
    tr[cur].lmax = max(max(tr[l].lmax, tr[r].lmax), tr[l].maxi - 2LL * tr[r].mini);
    tr[cur].rmax = max(max(tr[l].rmax, tr[r].rmax), tr[r].maxi - 2LL * tr[l].mini);
    tr[cur].ans = max(max(tr[l].ans, tr[r].ans), max(tr[l].maxi + tr[r].rmax, tr[r].maxi + tr[l].lmax));
}

void push_down(int cur) {
    if (!tr[cur].lazy) return;
    ll lazy = tr[cur].lazy;
    int l = cur<<1, r = l|1;
    tr[cur].lazy = 0; 
    tr[l].lazy += lazy; tr[r].lazy += lazy;
    tr[l].maxi += lazy; tr[r].maxi += lazy;
    tr[l].mini += lazy; tr[r].mini += lazy;
    tr[l].lmax -= lazy; tr[r].lmax -= lazy;
    tr[l].rmax -= lazy; tr[r].rmax -= lazy;
}

void build(int cur, int l, int r) {
    if (l == r) {
        tr[cur].mini = tr[cur].maxi = dep[l];
        tr[cur].lmax = tr[cur].rmax = -dep[l];
        tr[cur].ans = 0;
        return;
    }
    int mid = (l+r) >> 1;
    build(cur<<1, l, mid);
    build(cur<<1|1, mid+1, r);
    push_up(cur);
}

void update(int cur, int l, int r, int L, int R, ll x) {
    if (l >= L && r <= R) {
        tr[cur].lazy += x;
        tr[cur].maxi += x;
        tr[cur].mini += x;
        tr[cur].lmax -= x;
        tr[cur].rmax -= x;
        return;
    }
    push_down(cur);
    int mid = (l+r) >> 1;
    if (L <= mid) update(cur<<1, l, mid, L, R, x);
    if (R > mid) update(cur<<1|1, mid+1, r, L, R, x);
    push_up(cur);
}

ll query(int cur, int l, int r, int L, int R) {
    if (l >= L && r <= R) {
        return tr[cur].ans;
    }
    push_down(cur);
    int mid = (l+r) >> 1;
    ll res = 0;
    if (L <= mid) res += query(cur<<1, l, mid, L, R);
    if (R > mid) res += query(cur<<1|1, mid+1, r, L, R);
    push_up(cur);
    return res;
}

ll last = 0;
int main() {
    cin >> n >> q >> W;
    for (int i = 1; i < n; i++) {
        int u,v; ll w; cin >> u >> v >> w;
        addEdge(u,v,w); addEdge(v,u,w);
    }
    dfs(1, 0, 0);
    m = 2*n - 1;
    build(1, 1, m);

    while (q--) {
        ll d,e; cin >> d >> e;
        d = (d + last) % (n-1) + 1;
        d <<= 1;  // 编号从 2 开始
        e = (e + last) % W;
        ll x = e - edges[d].w;
        edges[d].w = edges[d^1].w = e;
        int u = edges[d].from, v = edges[d].to;
        if (ori_dep[u] > ori_dep[v]) swap(u,v);
        update(1, 1, m, in[v], out[v], x);
        last = query(1, 1, m, 1, m);
        cout << last << "\n";
        // 将第 d 条边的 weight 改为 e
    }
}

例3 Lightning Routing I

题意

给定 $n$ 个节点的有权树,和 $q$ 次询问。

每次询问有 $2$ 种:

$C ~ e_i ~ w_i$:将第 $e_i$ 条边的权值改为 $w_i$

$Q ~ v_i$:询问距离 $v_i$ 最远的节点的距离。

其中,$1 \leq n \leq 10^5, 1 \leq q \leq 10^5$。

题解

和上一题完全一样。

需要注意的是 $Q ~ v_i$ 这个询问。由树直径的基本性质,对于任意一个节点 $v$,距离最远的节点一定是直径的其中一端。那么问题可以转化为:

先求出直径的两端,然后判断一下哪一端距离 $v$ 最远。

• 这样的话,法一欧拉序就需要在线段树区间合并的时候,加一些额外的信息来 track 具体是哪些index被用到了转移中。

法一(欧拉序)代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;
const int maxm = 1e6;

struct Edge {
    int from, to, nxt;
    ll w;
} edges[maxn<<1];
int head[maxn], ecnt = 2, n, q;
ll W;
void addEdge(int u, int v, ll w) {
    Edge e = {u, v, head[u], w};
    edges[ecnt] = e;
    head[u] = ecnt++;
}

ll dep[maxn<<1];  // 欧拉序的 dep
int in[maxn], out[maxn];  // in[u]: 节点 u 在欧拉序中的起点
int ori_dep[maxn];  // 原本在树中的depth(不算weight)
int id = 0;

void dfs(int u, int p, ll d) {
    dep[++id] = d;
    in[u] = out[u] = id;
    for (int e = head[u]; e; e = edges[e].nxt) {
        int to = edges[e].to;
        if (to == p) continue;
        ll w = edges[e].w;
        ori_dep[to] = ori_dep[u] + 1;
        dfs(to, u, d + w);
        dep[++id] = d;
        out[u] = id;
    }
}

int m;
struct tree_node {
    ll mini, maxi, lmax, rmax, ans;
    ll lazy = 0;
    int mini_idx, maxi_idx, lmax_idx, rmax_idx, L, R;
} tr[maxn<<3];

void push_up(int cur) {
    int l = cur<<1, r = l+1;
    if (tr[l].mini < tr[r].mini) {
        tr[cur].mini = tr[l].mini;
        tr[cur].mini_idx = tr[l].mini_idx;
    } else {
        tr[cur].mini = tr[r].mini;
        tr[cur].mini_idx = tr[r].mini_idx;
    }

    if (tr[l].maxi > tr[r].maxi) {
        tr[cur].maxi = tr[l].maxi;
        tr[cur].maxi_idx = tr[l].maxi_idx;
    } else {
        tr[cur].maxi = tr[r].maxi;
        tr[cur].maxi_idx = tr[r].maxi_idx;
    }

    if (tr[l].lmax > tr[r].lmax) {
        tr[cur].lmax = tr[l].lmax;
        tr[cur].lmax_idx = tr[l].lmax_idx;
    } else {
        tr[cur].lmax = tr[r].lmax;
        tr[cur].lmax_idx = tr[r].lmax_idx;
    }
    if (tr[l].maxi - 2LL * tr[r].mini > tr[cur].lmax) {
        tr[cur].lmax = tr[l].maxi - 2LL * tr[r].mini;
        tr[cur].lmax_idx = tr[l].maxi_idx;
    }

    if (tr[l].rmax > tr[r].rmax) {
        tr[cur].rmax = tr[l].rmax;
        tr[cur].rmax_idx = tr[l].rmax_idx;
    } else {
        tr[cur].rmax = tr[r].rmax;
        tr[cur].rmax_idx = tr[r].rmax_idx;
    }
    if (tr[r].maxi - 2LL * tr[l].mini > tr[cur].rmax) {
        tr[cur].rmax = tr[r].maxi - 2LL * tr[l].mini;
        tr[cur].rmax_idx = tr[r].maxi_idx;
    }

    if (tr[l].ans > tr[r].ans) {
        tr[cur].L = tr[l].L;
        tr[cur].R = tr[l].R;
        tr[cur].ans = tr[l].ans;
    } else {
        tr[cur].L = tr[r].L;
        tr[cur].R = tr[r].R;
        tr[cur].ans = tr[r].ans;
    }
    if (tr[l].maxi + tr[r].rmax > tr[cur].ans) {
        tr[cur].ans = tr[l].maxi + tr[r].rmax;
        tr[cur].L = tr[l].maxi_idx;
        tr[cur].R = tr[r].rmax_idx;
    } 
    if (tr[r].maxi + tr[l].lmax > tr[cur].ans) {
        tr[cur].ans = tr[r].maxi + tr[l].lmax;
        tr[cur].L = tr[l].lmax_idx;
        tr[cur].R = tr[r].maxi_idx;
    }
}

void push_down(int cur) {
    if (!tr[cur].lazy) return;
    ll lazy = tr[cur].lazy;
    int l = cur<<1, r = l|1;
    tr[cur].lazy = 0; 
    tr[l].lazy += lazy; tr[r].lazy += lazy;
    tr[l].maxi += lazy; tr[r].maxi += lazy;
    tr[l].mini += lazy; tr[r].mini += lazy;
    tr[l].lmax -= lazy; tr[r].lmax -= lazy;
    tr[l].rmax -= lazy; tr[r].rmax -= lazy;
}

void build(int cur, int l, int r) {
    if (l == r) {
        tr[cur].mini = tr[cur].maxi = dep[l];
        tr[cur].lmax = tr[cur].rmax = -dep[l];
        tr[cur].ans = 0;
        tr[cur].mini_idx = tr[cur].maxi_idx = tr[cur].lmax_idx = tr[cur].rmax_idx = tr[cur].L = tr[cur].R = l;
        return;
    }
    int mid = (l+r) >> 1;
    build(cur<<1, l, mid);
    build(cur<<1|1, mid+1, r);
    push_up(cur);
}

void update(int cur, int l, int r, int L, int R, ll x) {
    if (l >= L && r <= R) {
        tr[cur].lazy += x;
        tr[cur].maxi += x;
        tr[cur].mini += x;
        tr[cur].lmax -= x;
        tr[cur].rmax -= x;
        return;
    }
    push_down(cur);
    int mid = (l+r) >> 1;
    if (L <= mid) update(cur<<1, l, mid, L, R, x);
    if (R > mid) update(cur<<1|1, mid+1, r, L, R, x);
    push_up(cur);
}

ll query(int cur, int l, int r, int L, int R) {
    if (l >= L && r <= R) {
        return tr[cur].ans;
    }
    push_down(cur);
    int mid = (l+r) >> 1;
    ll res = 0;
    if (L <= mid) res += query(cur<<1, l, mid, L, R);
    if (R > mid) res += query(cur<<1|1, mid+1, r, L, R);
    push_up(cur);
    return res;
}

ll query_min(int cur, int l, int r, int L, int R) {
    if (l >= L && r <= R) {
        return tr[cur].mini;
    }
    push_down(cur);
    int mid = (l+r) >> 1;
    ll r1 = 1e18, r2 = 1e18;
    if (L <= mid) r1 = query_min(cur<<1, l, mid, L, R);
    if (R > mid) r2 = query_min(cur<<1|1, mid+1, r, L, R);
    push_up(cur);
    return min(r1, r2);
}

ll dis(int l, int r) {
    if (l > r) swap(l,r);
    ll al = query_min(1, 1, m, l, l), ar = query_min(1, 1, m, r, r);
    ll mi = query_min(1, 1, m, l, r);
    return al + ar - 2LL * mi;
}

int main() {
    fastio;
    cin >> n;
    for (int i = 1; i < n; i++) {
        int u,v; ll w; cin >> u >> v >> w;
        addEdge(u,v,w); addEdge(v,u,w);
    }
    dfs(1, 0, 0);
    m = 2*n - 1;
    assert(id == m);
    build(1, 1, m);

    cin >> q;
    while (q--) {
        char op;
        cin >> op;
        if (op == 'C') {
            // 将第 d 条边的 weight 改为 e
            ll d,e; cin >> d >> e;
            d <<= 1;  // 编号从 2 开始
            ll x = e - edges[d].w;
            edges[d].w = edges[d^1].w = e;
            int u = edges[d].from, v = edges[d].to;
            if (ori_dep[u] > ori_dep[v]) swap(u,v);
            update(1, 1, m, in[v], out[v], x);
        } else {
            int v; cin >> v;
            v = in[v];
            int L = tr[1].L, R = tr[1].R;
            ll dis1 = dis(L, v);
            ll dis2 = dis(v, R);
            cout << max(dis1, dis2) << "\n";
        }
    }
}
法二(DFS序)代码
#include <bits/stdc++.h>
using namespace std;

const int maxn = 1e5+5;
const int maxm = 1e6;

struct Edge {
    int from, to, nxt;
    ll w;
} edges[maxn<<1];
int head[maxn], ecnt = 2, n, q;
void addEdge(int u, int v, ll w) {
    Edge e = {u, v, head[u], w};
    edges[ecnt] = e;
    head[u] = ecnt++;
}

ll dep[maxn];
int par[maxn][19], idcnt = 0, in[maxn], out[maxn], ver[maxn];  // in[u] ..., ver[id]: the id which corresponds to u
int ori_dep[maxn];  // 原本在树中的depth(不算weight)

void dfs(int u, int p) {
    in[u] = ++idcnt;
    ver[idcnt] = u;
    par[u][0] = p;
    for (int j = 1; j <= 18; j++) par[u][j] = par[par[u][j-1]][j-1];
    for (int e = head[u]; e; e = edges[e].nxt) {
        int to = edges[e].to;
        ll w = edges[e].w;
        if (to == p) continue;
        ori_dep[to] = ori_dep[u] + 1;
        dep[to] = dep[u] + w;
        dfs(to, u);
    }
    out[u] = idcnt;
}

int jump(int u, int d) {
    for (int j = 0; j <= 18; j++) {
        if (d & (1<<j)) u = par[u][j];
    }
    return u;
}

int lca(int u, int v) {
    if (ori_dep[u] < ori_dep[v]) swap(u,v);
    u = jump(u, ori_dep[u] - ori_dep[v]);
    for (int j = 18; j >= 0; j--) {
        if (par[u][j] != par[v][j]) u = par[u][j], v = par[v][j];
    }
    if (u == v) return u;
    return par[u][0];
}

struct tree_1 {
    struct tree_node_1 {
        ll d = 0, lazy = 0;
    };
    tree_node_1 tr[maxn<<2];
    void build(int cur, int l, int r) {
        if (l == r) {
            tr[cur].d = dep[ver[l]];
            return;
        }
        int mid = (l+r) >> 1;
        build(cur<<1, l, mid);
        build(cur<<1|1, mid+1, r);
    }
    void push_down(int cur) {
        if (!tr[cur].lazy) return;
        int l = cur<<1, r = l|1;
        ll lazy = tr[cur].lazy;
        tr[l].lazy += lazy, tr[r].lazy += lazy;
        tr[l].d += lazy, tr[r].d += lazy;
        tr[cur].lazy = 0;
    }
    void update(int cur, int l, int r, int L, int R, ll delta) {
        if (l >= L && r <= R) {
            tr[cur].lazy += delta;
            tr[cur].d += delta;
            return;
        }
        push_down(cur);
        int mid = (l+r) >> 1;
        if (L <= mid) update(cur<<1, l, mid, L, R, delta);
        if (R > mid) update(cur<<1|1, mid+1, r, L, R, delta);
    }
    ll query(int cur, int l, int r, int p) {
        if (l == r) return tr[cur].d;
        push_down(cur);
        int mid = (l+r) >> 1;
        if (p <= mid) return query(cur<<1, l, mid, p);
        return query(cur<<1|1, mid+1, r, p);
    }
    ll query_dis(int id1, int id2) {
        ll d1 = query(1, 1, n, id1), d2 = query(1, 1, n, id2);
        ll u1 = ver[id1], u2 = ver[id2];
        ll x = lca(u1, u2);
        int idx = in[x];
        ll dx = query(1, 1, n, idx);
        return d1 + d2 - 2LL * dx;
    }
} tr1;

struct tree_2 {
    struct tree_node_2 {
        int id1, id2;  // 直径的 id
    };
    tree_node_2 tr[maxn<<2];
    void push_up(int cur) {
        int l = cur<<1, r = cur<<1|1;
        int l_id1 = tr[l].id1, l_id2 = tr[l].id2, r_id1 = tr[r].id1, r_id2 = tr[r].id2;
        ll len = -1;
        ll dis1 = tr1.query_dis(l_id1, r_id1);
        ll dis2 = tr1.query_dis(l_id1, r_id2);
        ll dis3 = tr1.query_dis(l_id2, r_id1);
        ll dis4 = tr1.query_dis(l_id2, r_id2);
        ll dis5 = tr1.query_dis(l_id1, l_id2);
        ll dis6 = tr1.query_dis(r_id1, r_id2);
        if (dis1 > len) tr[cur] = {l_id1, r_id1}, len = dis1;
        if (dis2 > len) tr[cur] = {l_id1, r_id2}, len = dis2;
        if (dis3 > len) tr[cur] = {l_id2, r_id1}, len = dis3;
        if (dis4 > len) tr[cur] = {l_id2, r_id2}, len = dis4;
        if (dis5 > len) tr[cur] = {l_id1, l_id2}, len = dis5;
        if (dis6 > len) tr[cur] = {r_id1, r_id2}, len = dis6;
    }
    void build(int cur, int l, int r) {
        if (l == r) {
            tr[cur].id1 = tr[cur].id2 = l;
            return;
        }
        int mid = (l+r) >> 1;
        build(cur<<1, l, mid);
        build(cur<<1|1, mid+1, r);
        push_up(cur);
    }
    void update(int cur, int l, int r, int L, int R) {
        if (l >= L && r <= R) return;  // 完全覆盖不用更新
        int mid = (l+r) >> 1;
        if (L <= mid) update(cur<<1, l, mid, L, R);
        if (R > mid) update(cur<<1|1, mid+1, r, L, R);
        push_up(cur);
    }
} tr2;

int main() {
    cin >> n;
    for (int i = 1; i < n; i++) {
        int u,v; ll w; cin >> u >> v >> w;
        addEdge(u,v,w); addEdge(v,u,w);
    }
    dfs(1, 0);
    tr1.build(1, 1, n);
    tr2.build(1, 1, n);

    cin >> q;
    while (q--) {
        char op;
        cin >> op;
        if (op == 'C') {
            // 将第 d 条边的 weight 改为 e
            ll d,e; cin >> d >> e;
            d <<= 1;  // 编号从 2 开始
            ll x = e - edges[d].w;
            edges[d].w = edges[d^1].w = e;
            int u = edges[d].from, v = edges[d].to;
            if (ori_dep[u] > ori_dep[v]) swap(u,v);
            tr1.update(1, 1, n, in[v], out[v], x);
            tr2.update(1, 1, n, in[v], out[v]);
        } else {
            int v; cin >> v;
            v = in[v];  // v 的 id 编号
            int id1 = tr2.tr[1].id1, id2 = tr2.tr[1].id2;
            ll dis1 = tr1.query_dis(id1, v);
            ll dis2 = tr1.query_dis(v, id2);
            cout << max(dis1, dis2) << "\n";
        }
    }
}

参考链接

  1. https://www.luogu.com.cn/problem/solution/P6845