会议

GitHub跳转原题关系图返回列表

距离和最小的点即树的重心:用重心模板求重心再算距离和;换根 DP 递推全部点的距离和;最省的一版只跑一遍 DFS,从根沿重子树下降到重心,边走边累加增量。

OJ: luogu

题目 ID: P1395

难度:普及

标签:换根 DP树形 DP树树的重心

创建: 2026-07-16 23:59

更新: 2026-10-30 08:52

RBook 文章:
树的重心
树的中心
换根 DP

形式化题目

给定一棵 nn 个点、n−1n-1 条边的无向树,每条边长度都是 11。对每个点 xx,记

S(x)=∑u=1ndist(u,x)S(x) = \sum_{u=1}^{n} \mathrm{dist}(u, x)

即所有点到 xx 的距离和。要求找出使 S(x)S(x) 最小的点,输出它的编号与 S(x)S(x);若有多个点同样最小,输出编号最小的那个。

暴力

先看一个可以直接验证想法的朴素解:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-08-12 22:27
 * update_at: 2026-08-12 22:27
 */
// brute.cpp:小数据暴力解,枚举每个点作为会议地点,再 BFS 计算距离和。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 50005;

int n;
vector<int> g[MAXN]; // 树的邻接表

// 以 start 为会议地点做一次 BFS,返回所有点到它的距离和。
long long calc_sum_from(int start) {
    static int dist[MAXN]; // dist[u]:start 到 u 的距离,-1 表示尚未访问
    for (int i = 1; i <= n; i++) dist[i] = -1;

    queue<int> q;
    q.push(start);
    dist[start] = 0;

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int i = 0; i < (int)g[u].size(); i++) {
            int v = g[u][i];
            if (dist[v] != -1) continue;
            dist[v] = dist[u] + 1;
            q.push(v);
        }
    }

    long long sum = 0;
    for (int i = 1; i <= n; i++) sum += dist[i];
    return sum;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    // 枚举每个点作为会议地点;距离和更小才更新,相等时保留编号小者。
    int best_node = 1;
    long long best_sum = calc_sum_from(1);
    for (int i = 2; i <= n; i++) {
        long long now = calc_sum_from(i);
        if (now < best_sum) {
            best_sum = now;
            best_node = i;
        }
    }

    cout << best_node << " " << best_sum << "\n";

    return 0;
}

brute.cpp 枚举每个点作为会议地点,对每个点各做一次 BFS 求距离和,复杂度 O(n2)O(n^2),无法通过 n=5×104n = 5 \times 10^4。

思路

本题有三种解法,复杂度都是 O(n)O(n),差别只在于“跑几遍树”。正式主解是解法一(重心判定,main.cpp),它依赖一条数学性质;解法二(换根 DP,main-bfs-dp.cpp 与 main-dfs-dp.cpp) 是更通用的树形 DP 套路,不依赖该性质也能求出答案;**解法三(main-descent.cpp)**把解法一的符号分析再用一次,只跑一遍 DFS 就够。

解法一:重心判定

思路

关键观察:距离和最小的点就是树的重心。先证明这个结论——相邻两点换根时,距离和可以 O(1)O(1) 转移。设 vv 是 uu 的儿子,把树以 11 为根,会议地点从 uu 移到 vv。对树中的任意点 xx,xx 到 vv 与 xx 到 uu 的距离之差,由 xx 是否在 vv 的子树内决定:

  • xx 在 vv 子树内(共 subtree_size[v]\mathrm{subtree\_size}[v] 个点):xx 到 uu 的路径先到 vv 再经过边 (v,u)(v,u),移到 vv 后省掉这条边,距离减少 11,即 dist(x,v)=dist(x,u)−1\mathrm{dist}(x,v) = \mathrm{dist}(x,u) - 1;
  • xx 不在 vv 子树内(共 n−subtree_size[v]n - \mathrm{subtree\_size}[v] 个点):xx 到 vv 的路径要绕道 uu 再经边 (u,v)(u,v),距离增加 11,即 dist(x,v)=dist(x,u)+1\mathrm{dist}(x,v) = \mathrm{dist}(x,u) + 1。

把每个点的新距离代入 S(v)S(v)(记 S(v)S(v) 为"会议地点定在 vv 时所有点到 vv 的距离和",即形式化题目中的 S(x)S(x) 取 x=vx = v),按这两组分别求和:

S(v)=∑x∈v子树(dist(x,u)−1)+∑x∉v子树(dist(x,u)+1)S(v) = \sum_{x \in \text{v子树}} \big(\mathrm{dist}(x,u) - 1\big) + \sum_{x \notin \text{v子树}} \big(\mathrm{dist}(x,u) + 1\big)

第一组有 subtree_size[v]\mathrm{subtree\_size}[v] 项各减 11,第二组有 n−subtree_size[v]n - \mathrm{subtree\_size}[v] 项各加 11,所以:

S(v)=S(u)−subtree_size[v]+(n−subtree_size[v])=S(u)+n−2⋅subtree_size[v]S(v) = S(u) - \mathrm{subtree\_size}[v] + \big(n - \mathrm{subtree\_size}[v]\big) = S(u) + n - 2 \cdot \mathrm{subtree\_size}[v]

这就是换根公式 dist_sum[v]=dist_sum[u]+n−2⋅subtree_size[v]\mathrm{dist\_sum}[v] = \mathrm{dist\_sum}[u] + n - 2 \cdot \mathrm{subtree\_size}[v]:它把"重新算一遍 S(v)S(v)“化简成"在 S(u)S(u) 上做一次由子树规模差驱动的增量修正”。

换根公式中,S(u)→S(v)S(u) \to S(v) 这个方向上的变化量是 n−2⋅subtree_size[v]n - 2 \cdot \mathrm{subtree\_size}[v],它的符号决定沿这条边移动值会不会变好:

  • 若 subtree_size[v]>n/2\mathrm{subtree\_size}[v] > n/2,变化量为负,移到 vv 会减小距离和;
  • 若 subtree_size[v]<n/2\mathrm{subtree\_size}[v] < n/2,变化量为正,移到 vv 会增大距离和;
  • 若恰好等于 n/2n/2,变化量为零,移到 vv 距离和不变。

因此距离和最小的点正是删掉它后所有连通块大小都不超过 n/2n/2 的重心——"距离和最小"与"最大连通块 ≤ n/2"是同一组点的两种刻画。

于是只需两步:用重心模板求出所有重心(重心至多两个且相邻,两个重心距离和相同,取编号最小者),再对选中的点做一次 BFS 求距离和。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-08-12 22:27
 * update_at: 2026-08-16 00:10
 */
// main.cpp:距离和最小的点 = 树的重心(P1395 会议)。
// 用 rbook 模板 tree-centroid2 求出所有重心,取编号最小者(多重心时距离和相同),
// 再一遍 BFS 求它的距离和。
#include <bits/stdc++.h>
using namespace std;

using Graph = std::vector<std::vector<int>>;
Graph tree;  // 全局邻接表:使用前先 resize(n+1) 并加边

// 求树的所有重心。
// 重心:删除该点后,剩下的每个连通块大小都不超过 n/2。
// 与 tree_centroid.cpp 不同:不维护全局最小值,直接按"最大连通块 ≤ n/2"判定重心。
struct TreeCentroid2 {
    int n;
    std::vector<int> sz;   // sz[u] = u 的子树大小
    std::vector<int> ans;  // 答案:所有重心,按编号升序

    explicit TreeCentroid2(int n) : n(n), sz(n + 1) {}

    // 返回所有重心(编号升序)
    std::vector<int> find_centroids(int root = 1) {
        ans.clear();
        dfs(root, 0);
        std::sort(ans.begin(), ans.end());
        return ans;
    }

    // 统计子树大小;若 B(u) = 删除 u 后最大的连通块大小 ≤ n/2,u 就是重心
    void dfs(int u, int parent) {
        sz[u] = 1;
        int mx = 0;  // B(u):先看各儿子子树

        for (int v : tree[u]) {
            if (v == parent) continue;
            dfs(v, u);
            sz[u] += sz[v];
            mx = std::max(mx, sz[v]);
        }

        // 父亲方向也是一块:整棵树减去 u 的子树
        mx = std::max(mx, n - sz[u]);

        // 等价判定:B(u) ≤ n/2 ⟺ u 是重心
        if (mx <= n / 2) ans.push_back(u);
    }
};

// 从 s 出发 BFS,返回所有点到 s 的距离和(s 本身的深度 0 不计入)。
long long dist_sum_from(int s) {
    queue<int> q;
    vector<int> dist(tree.size(), -1);
    dist[s] = 0;
    q.push(s);
    long long total = 0;

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v : tree[u]) {
            if (dist[v] != -1) continue;
            dist[v] = dist[u] + 1;
            total += dist[v];
            q.push(v);
        }
    }
    return total;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    tree.resize(n + 1);
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        tree[u].push_back(v);
        tree[v].push_back(u);
    }

    // 距离和最小的点 = 重心;重心至多两个且相邻、距离和相同,取编号最小的。
    TreeCentroid2 tc(n);
    vector<int> cs = tc.find_centroids();
    int meeting = cs[0];

    cout << meeting << ' ' << dist_sum_from(meeting) << '\n';
    return 0;
}

复杂度

  • 时间:求重心 DFS 一次 O(n)O(n),求距离和 BFS 一次 O(n)O(n),总 O(n)O(n)。
  • 空间:邻接表与子树大小数组 O(n)O(n)。

解法二:换根 DP

思路

不利用重心定理,把每个点的距离和全部算出来再扫描取最小,属于通用的"树形 DP + 换根"套路。先把树以 11 为根,用 BFS 求出遍历序、父亲、深度,顺带得到 dist_sum[1](等于所有点的深度之和);逆序遍历序累加子树大小 subtree_size;再正序遍历序,用解法一推导出的换根公式 dist_sum[v] = dist_sum[u] + n - 2 * subtree_size[v](沿父子边移动时,vv 子树内 sz[v]sz[v] 个点距离各 −1-1、其余点各 +1+1)沿父子边递推全部点的距离和。

以样例链 1−2−3−41 - 2 - 3 - 4 为例,以 11 为根做一次换根 DP。先画出这棵树,括号里标出每个点的子树大小(以 11 为根):

text
        1 (sz=4)
        |
        2 (sz=3)
        |
        3 (sz=2)
        |
        4 (sz=1)

从根 11 往下看,这棵树是一根链:每个点的子树就是它自己加上它下方的所有点,所以 sz 沿根向叶子递减 4,3,2,14,3,2,1。换根 DP 从根出发逐层往下递推:1→2→3→41 \to 2 \to 3 \to 4,每步都只跨一条边。

换根 DP 得到的结果如下:

节点 uu 1 2 3 4
subtree_size[u]\mathrm{subtree\_size}[u] 4 3 2 1
dist_sum[u]\mathrm{dist\_sum}[u] 6 4 4 6

先看 subtree_size 一行:叶子 44 是 11,向上累加得到 33、22、44。再看 dist_sum 一行:dist_sum[1] = 0+1+2+3 = 6,然后 dist_sum[2]=6+4−2×3=4\mathrm{dist\_sum}[2] = 6 + 4 - 2\times3 = 4,dist_sum[3]=4+4−2×2=4\mathrm{dist\_sum}[3] = 4 + 4 - 2\times2 = 4,dist_sum[4]=4+4−2×1=6\mathrm{dist\_sum}[4] = 4 + 4 - 2\times1 = 6。节点 22 与 33 距离和同为 44,取编号小的 22,与样例输出 2 4 一致。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-08-12 22:27
 * update_at: 2026-08-12 22:27
 */
// main-bfs-dp.cpp:换根 DP 求树上所有点到某点的距离和最小值(P1395 会议)。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 50005;

int n;
vector<int> g[MAXN];      // 树的邻接表
int parent_node[MAXN];    // parent_node[u]:以 1 为根时 u 的父亲,根的父节点为 0
int depth_arr[MAXN];      // depth_arr[u]:以 1 为根时 u 的深度
int order_arr[MAXN];      // order_arr[]:BFS 遍历序,第 1 个是根 1
int order_cnt;            // BFS 访问到的节点个数
int subtree_size[MAXN];   // subtree_size[u]:以 1 为根时 u 子树内的节点数
long long dist_sum[MAXN]; // dist_sum[u]:所有节点到 u 的距离和

// BFS 求遍历序、父亲、深度,并顺带求 dist_sum[1](所有点深度之和)。
void bfs_root() {
    queue<int> q;
    q.push(1);
    parent_node[1] = 0;
    depth_arr[1] = 0;
    order_cnt = 0;
    dist_sum[1] = 0;

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        order_arr[++order_cnt] = u;
        dist_sum[1] += depth_arr[u];

        for (int i = 0; i < (int)g[u].size(); i++) {
            int v = g[u][i];
            if (v == parent_node[u]) continue;
            parent_node[v] = u;
            depth_arr[v] = depth_arr[u] + 1;
            q.push(v);
        }
    }
}

// 逆序遍历序,自底向上累加每棵子树的大小。
void calc_subtree_size() {
    for (int i = 1; i <= n; i++) subtree_size[i] = 1;
    for (int i = order_cnt; i >= 1; i--) {
        int u = order_arr[i];
        if (parent_node[u] != 0) {
            subtree_size[parent_node[u]] += subtree_size[u];
        }
    }
}

// 换根 DP:根从 u 移到儿子 v 时,用公式推出 v 的距离和。
void reroot_dp() {
    for (int i = 1; i <= order_cnt; i++) {
        int u = order_arr[i];
        for (int j = 0; j < (int)g[u].size(); j++) {
            int v = g[u][j];
            if (parent_node[v] != u) continue; // 只走父亲 -> 儿子方向

            // 换根公式:v 子树内 subtree_size[v] 个点距离各 -1,
            // 其余 n - subtree_size[v] 个点距离各 +1,
            // 所以 dist_sum[v] = dist_sum[u] + n - 2 * subtree_size[v]。
            dist_sum[v] = dist_sum[u] + (long long)n - 2LL * subtree_size[v];
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    bfs_root();
    calc_subtree_size();
    reroot_dp();

    // 距离和取最小;距离和相等时保留编号最小的点(只严格小于才更新)。
    int best_node = 1;
    long long best_sum = dist_sum[1];
    for (int i = 2; i <= n; i++) {
        if (dist_sum[i] < best_sum) {
            best_sum = dist_sum[i];
            best_node = i;
        }
    }

    cout << best_node << " " << best_sum << "\n";

    return 0;
}

上面的 BFS 版本用遍历序迭代完成三步;换根 DP 也可以写成两次 DFS 的版本,更贴近手写递归习惯:第一次 DFS 回溯时累加子树大小、向下时累加深度顺带求出 dist_sum[1],第二次 DFS 从根向叶子用换根公式递推所有点的距离和。

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-08-16 00:40
 * update_at: 2026-08-16 00:40
 */
// main-dfs-dp.cpp:换根 DP 的两次 DFS 实现(P1395 会议)。
// 第一次 DFS 求子树大小与 dist_sum[1](所有点深度之和),
// 第二次 DFS 沿父子边用换根公式递推全部点的距离和。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 50005;

int n;
vector<int> g[MAXN];      // 树的邻接表
int subtree_size[MAXN];   // subtree_size[u]:以 1 为根时 u 子树内的节点数
long long dist_sum[MAXN]; // dist_sum[u]:所有节点到 u 的距离和

// 第一次 DFS:求子树大小,并累加深度得到 dist_sum[1]。
// dep 表示 u 的深度,根 1 的深度为 0。
void dfs_subtree(int u, int fa, int dep) {
    subtree_size[u] = 1;
    dist_sum[1] += dep;

    for (int i = 0; i < (int)g[u].size(); i++) {
        int v = g[u][i];
        if (v == fa) continue;
        dfs_subtree(v, u, dep + 1);
        subtree_size[u] += subtree_size[v];
    }
}

// 第二次 DFS:换根递推,从 u 移到儿子 v 时用公式推 dist_sum[v]。
void dfs_reroot(int u, int fa) {
    for (int i = 0; i < (int)g[u].size(); i++) {
        int v = g[u][i];
        if (v == fa) continue;

        // 换根公式:v 子树内 subtree_size[v] 个点距离各 -1,
        // 其余 n - subtree_size[v] 个点距离各 +1。
        dist_sum[v] = dist_sum[u] + (long long)n - 2LL * subtree_size[v];
        dfs_reroot(v, u);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    dfs_subtree(1, 0, 0);
    dfs_reroot(1, 0);

    // 距离和取最小;距离和相等时保留编号最小的点(只严格小于才更新)。
    int best_node = 1;
    long long best_sum = dist_sum[1];
    for (int i = 2; i <= n; i++) {
        if (dist_sum[i] < best_sum) {
            best_sum = dist_sum[i];
            best_node = i;
        }
    }

    cout << best_node << " " << best_sum << "\n";

    return 0;
}

复杂度

  • 时间:BFS、求子树大小、换根 DP、扫描答案各 O(n)O(n),总 O(n)O(n)。
  • 空间:邻接表与四个 O(n)O(n) 数组,总 O(n)O(n)。

解法三:单次 DFS + 沿重子树下降

思路

解法一已经用换根公式的符号把话说清楚了:从 uu 移到儿子 vv,距离和的变化量是 n−2⋅subtree_size[v]n - 2\cdot\mathrm{subtree\_size}[v],为负就变好。既然如此,就没必要先把所有重心都枚举出来再挑:站在根上,只要有个儿子的子树超过 n/2n/2 就往那个儿子走,走不动了,脚下的点就是重心。

停下就一定停在正确位置,两头都要交代:

  • 不会白走:每走一步变化量都 ≤0\le 0,距离和单调不增,所以停下时的值不劣于路上任何一个点;
  • 停下时正好满足重心定义:停下的点 uu 的所有儿子 cc 都满足 2 subtree_size[c]≤n2\,\mathrm{subtree\_size}[c] \le n;而“身后”那块(父亲方向)的规模是 n−subtree_size[u]n - \mathrm{subtree\_size}[u],它不超过 n/2n/2 是因为走进 uu 的那一步要求变化量 ≤0\le 0,即 subtree_size[u]≥n/2\mathrm{subtree\_size}[u] \ge n/2(起点没有身后那块,规模为 00)。两块合起来正是“删掉 uu 后每块都不超过 n/2n/2”。

距离和也不用重新遍历算:求子树大小的那次 DFS 顺手用深度和得到 dist_sum[1],之后每走一步把增量 n−2 subtree_size[v]n - 2\,\mathrm{subtree\_size}[v] 加到答案上即可。

唯一要留神的是两个重心的情形(nn 为偶数,某条边两侧各 n/2n/2 个点):沿下降路径先到的是离根近的那个,它的编号不一定更小。这时变化量刚好是 00(距离和相同),题目要求输出编号最小的点,所以只在儿子的编号比当前点小时才继续走一步。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-29 22:41
 * update_at: 2026-10-29 22:41
 */
// main-descent.cpp:只跑一遍 DFS 的解法(P1395 会议)。
// 一次 DFS 求出子树大小与 dist_sum[1](所有点深度之和),
// 然后从根沿"重子树"往下走:每走一步用 dist_sum 的增量判断值是否变小,
// 走到不能再变好时停下的点就是重心,走到哪算到哪,不需要第二遍遍历。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 50005;

int n;
vector<int> g[MAXN];      // 树的邻接表
int fa[MAXN];             // 以 1 为根时的父亲
int subtree_size[MAXN];   // subtree_size[u]:以 1 为根时 u 子树内的节点数
long long dist_sum_root;  // 所有点到根 1 的距离和,等于所有点的深度之和

// 一次 DFS:记录父亲、求子树大小,顺带把深度累加成 dist_sum[1]。
void dfs(int u, int f, int dep) {
    fa[u] = f;
    subtree_size[u] = 1;
    dist_sum_root += dep;

    for (int v : g[u]) {
        if (v == f) continue;
        dfs(v, u, dep + 1);
        subtree_size[u] += subtree_size[v];
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    dfs(1, 0, 0);

    // 从根往下走:从 u 移到儿子 v 时,距离和变化量为 n - 2 * subtree_size[v]。
    // 增量为负才变好;为 0 时距离和不变,但题目要输出编号最小的重心,
    // 这时只有 v 的编号比 u 小才值得移过去。
    int cur = 1;
    long long best_sum = dist_sum_root;
    while (true) {
        // 找出子树最大的儿子:只有它可能满足 2 * sz > n
        int heavy = 0;
        for (int v : g[cur]) {
            if (v == fa[cur]) continue;
            if (heavy == 0 || subtree_size[v] > subtree_size[heavy]) heavy = v;
        }
        if (heavy == 0) break;  // 叶子,没有儿子可走

        long long delta = (long long)n - 2LL * subtree_size[heavy];
        if (delta > 0) break;                    // 再往下走距离和会变大
        if (delta == 0 && heavy > cur) break;    // 不变大但编号更大,保留当前点
        best_sum += delta;
        cur = heavy;
    }

    cout << cur << " " << best_sum << "\n";

    return 0;
}

复杂度

  • 时间:一次 DFS 求子树大小与深度和 O(n)O(n);下降路径长度不超过 nn,每步只扫当前点的儿子,路径上各点度数之和不超过 2n2n,仍是 O(n)O(n)。
  • 空间:邻接表、fa、subtree_size 共 O(n)O(n)——比解法一少一个 dist 数组和 BFS 队列。

复杂度对比

解法一(重心判定) 解法二(换根 DP) 解法三(单次下降)
时间复杂度 O(n)O(n) O(n)O(n) O(n)O(n)
空间复杂度 O(n)O(n) O(n)O(n) O(n)O(n)
依赖性质 距离和最小 = 重心 无(通用套路) 距离和最小 = 重心(只用符号分析)
遍历次数 2 遍:重心 DFS + 距离 BFS 2 遍:求子树 + 递推全部点 1 遍:DFS 之后只在下降路径上走
额外步骤 求重心 + 一次 BFS 算距离和 递推全部点的距离和 + 扫描取最小 沿路累加增量 + 处理编号最小
代码量 更短(模板化) 略长 最短

总结

“树上所有点到某点距离和"这类问题有两个方向:一是利用"距离和最小的点就是树的重心"这条性质,直接按重心定义(每个连通块 ≤ n/2)求点;二是通用套路——先以任意点为根求出子树大小,再用换根公式 dist_sum[v] = dist_sum[u] + n - 2 * subtree_size[v] 沿树边递推。难点只在把"重新算一棵树"化简成"相邻两棵子树规模差驱动的增量转移”。

解法三是两者的合流:既用重心这条性质(所以不递推全部点),又用增量转移(所以不必再遍历一遍算距离和)——只要求出子树大小,答案就藏在从根到重心这一段路径上。rbook 的《树的中心》讲的是另一个目标(到所有点最大距离最小),注意不要混淆。

图示解析

这张 ASCII 图展示整道题的三条解法路线:

text
朴素模拟(brute.cpp)
  枚举会议点 x,从 x 做一次 BFS 求距离和     每个点 O(n)
        |
        | 瓶颈:n 个点各 BFS 一次,O(n^2) 太大
        v
关键观察(换根公式)
  以 1 为根,dist_sum[1] = 所有点深度和
  v 是 u 的儿子时:
  dist_sum[v] = dist_sum[u] + n - 2 * subtree_size[v]
  (v 子树内距离 -1,其余点距离 +1)
        |
        +------------------------+------------------------+
        v                        v                        v
解法一:重心判定          解法二:换根 DP           解法三:单次下降
(main.cpp)              (main-*-dp.cpp)         (main-descent.cpp)
n-2*sz 符号定方向         BFS 求遍历序/父亲         一次 DFS 求 sz 与深度和
距离和最小 = 重心         逆序累加 subtree_size     沿重子树往下走
求所有重心取编号小者      正序递推全部 dist_sum     边走边加 n-2*sz 增量
再 BFS 一次算距离和       扫描取最小,相等留小号    停下即重心,不再扫全树
        |                        |                        |
        v                        v                        v
复杂度 O(n),空间 O(n)

图中先由换根公式引出三条分支:左边利用公式的符号分析得出"重心"结论,跳过逐点递推直接求点;中间保留完整递推,得到全部点的距离和;右边把两者合起来——只沿"变好"的方向走一条路径,走到头就是重心,距离和用增量累加。三者的共同核心都是"跨一条边距离和的 O(1)O(1) 增量转移",这比 O(n2)O(n^2) 次"完整 BFS"省掉了 nn 的因子。