距离和最小的点即树的重心:用重心模板求重心再算距离和;换根 DP 递推全部点的距离和;最省的一版只跑一遍 DFS,从根沿重子树下降到重心,边走边累加增量。
OJ: luogu
题目 ID: P1395
难度:普及
形式化题目
给定一棵
即所有点到
暴力
先看一个可以直接验证想法的朴素解:
/**
* 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 求距离和,复杂度
思路
本题有三种解法,复杂度都是 main.cpp),它依赖一条数学性质;解法二(换根 DP,main-bfs-dp.cpp 与 main-dfs-dp.cpp) 是更通用的树形 DP 套路,不依赖该性质也能求出答案;**解法三(main-descent.cpp)**把解法一的符号分析再用一次,只跑一遍 DFS 就够。
解法一:重心判定
思路
关键观察:距离和最小的点就是树的重心。先证明这个结论——相邻两点换根时,距离和可以
在 子树内(共 个点): 到 的路径先到 再经过边 ,移到 后省掉这条边,距离减少 ,即 ; 不在 子树内(共 个点): 到 的路径要绕道 再经边 ,距离增加 ,即 。
把每个点的新距离代入
第一组有
这就是换根公式
换根公式中,
- 若
,变化量为负,移到 会减小距离和; - 若
,变化量为正,移到 会增大距离和; - 若恰好等于
,变化量为零,移到 距离和不变。
因此距离和最小的点正是删掉它后所有连通块大小都不超过
于是只需两步:用重心模板求出所有重心(重心至多两个且相邻,两个重心距离和相同,取编号最小者),再对选中的点做一次 BFS 求距离和。
代码
/**
* 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 一次
,求距离和 BFS 一次 ,总 。 - 空间:邻接表与子树大小数组
。
解法二:换根 DP
思路
不利用重心定理,把每个点的距离和全部算出来再扫描取最小,属于通用的"树形 DP + 换根"套路。先把树以 dist_sum[1](等于所有点的深度之和);逆序遍历序累加子树大小 subtree_size;再正序遍历序,用解法一推导出的换根公式 dist_sum[v] = dist_sum[u] + n - 2 * subtree_size[v](沿父子边移动时,
以样例链
1 (sz=4)
|
2 (sz=3)
|
3 (sz=2)
|
4 (sz=1)从根 sz 沿根向叶子递减
换根 DP 得到的结果如下:
| 节点 |
1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 4 | 3 | 2 | 1 | |
| 6 | 4 | 4 | 6 |
先看 subtree_size 一行:叶子 dist_sum 一行:dist_sum[1] = 0+1+2+3 = 6,然后 2 4 一致。
代码
/**
* 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 从根向叶子用换根公式递推所有点的距离和。
/**
* 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、扫描答案各
,总 。 - 空间:邻接表与四个
数组,总 。
解法三:单次 DFS + 沿重子树下降
思路
解法一已经用换根公式的符号把话说清楚了:从
停下就一定停在正确位置,两头都要交代:
- 不会白走:每走一步变化量都
,距离和单调不增,所以停下时的值不劣于路上任何一个点; - 停下时正好满足重心定义:停下的点
的所有儿子 都满足 ;而“身后”那块(父亲方向)的规模是 ,它不超过 是因为走进 的那一步要求变化量 ,即 (起点没有身后那块,规模为 )。两块合起来正是“删掉 后每块都不超过 ”。
距离和也不用重新遍历算:求子树大小的那次 DFS 顺手用深度和得到 dist_sum[1],之后每走一步把增量
唯一要留神的是两个重心的情形(
代码
/**
* 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 求子树大小与深度和
;下降路径长度不超过 ,每步只扫当前点的儿子,路径上各点度数之和不超过 ,仍是 。 - 空间:邻接表、
fa、subtree_size共——比解法一少一个 dist数组和 BFS 队列。
复杂度对比
| 解法一(重心判定) | 解法二(换根 DP) | 解法三(单次下降) | |
|---|---|---|---|
| 时间复杂度 | |||
| 空间复杂度 | |||
| 依赖性质 | 距离和最小 = 重心 | 无(通用套路) | 距离和最小 = 重心(只用符号分析) |
| 遍历次数 | 2 遍:重心 DFS + 距离 BFS | 2 遍:求子树 + 递推全部点 | 1 遍:DFS 之后只在下降路径上走 |
| 额外步骤 | 求重心 + 一次 BFS 算距离和 | 递推全部点的距离和 + 扫描取最小 | 沿路累加增量 + 处理编号最小 |
| 代码量 | 更短(模板化) | 略长 | 最短 |
总结
“树上所有点到某点距离和"这类问题有两个方向:一是利用"距离和最小的点就是树的重心"这条性质,直接按重心定义(每个连通块 ≤ n/2)求点;二是通用套路——先以任意点为根求出子树大小,再用换根公式 dist_sum[v] = dist_sum[u] + n - 2 * subtree_size[v] 沿树边递推。难点只在把"重新算一棵树"化简成"相邻两棵子树规模差驱动的增量转移”。
解法三是两者的合流:既用重心这条性质(所以不递推全部点),又用增量转移(所以不必再遍历一遍算距离和)——只要求出子树大小,答案就藏在从根到重心这一段路径上。rbook 的《树的中心》讲的是另一个目标(到所有点最大距离最小),注意不要混淆。
图示解析
这张 ASCII 图展示整道题的三条解法路线:
朴素模拟(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)图中先由换根公式引出三条分支:左边利用公式的符号分析得出"重心"结论,跳过逐点递推直接求点;中间保留完整递推,得到全部点的距离和;右边把两者合起来——只沿"变好"的方向走一条路径,走到头就是重心,距离和用增量累加。三者的共同核心都是"跨一条边距离和的