枚举医院节点做 BFS 求加权距离和 O(n²);再用加权换根 DP 把答案压到 O(n):挪一条边只让一侧居民少走一步、另一侧多走一步。
启发记录: 换根dp的入门题目
OJ: luogu
题目 ID: P1364
难度:普及
形式化题目
给定一棵
其中
解法总览
题面
| 解法 | 核心观察 | 复杂度 | 适用场景 | 对应代码 |
|---|---|---|---|---|
| 解法一:枚举 + BFS | 树中两点路径唯一,BFS 的层数就是距离 | main.cpp、main.py |
||
| 解法二:加权换根 DP | 医院沿一条边挪动时,代价变化只由那一侧的居民数决定 | main-reroot.cpp |
正式主解是解法一:它直接对应“把
解法一:枚举医院 + 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-07-27 00:00
* update_at: 2026-10-29 22:55
*/
// main.cpp:枚举医院所在节点 + 从该点 BFS 求加权距离和(P1364 医院设置)。
// 题面 n <= 100,O(n^2) 直接可过;需要更大规模时的 O(n) 换根 DP 见 main-reroot.cpp。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 105;
ll n;
vector<int> tree[MAXN]; // tree[u]:与 u 相邻的节点(题面给的是两条链接,按无向树存)
ll population[MAXN]; // population[u]:节点 u 的居民数
ll dist[MAXN]; // dist[u]:本轮 BFS 中 u 到医院的边数
ll answer = LLONG_MAX; // 最小总路程
// 从 hospital 出发 BFS,返回所有居民走到 hospital 的路程和。
ll bfs_sum(ll hospital) {
for (ll i = 1; i <= n; i++) dist[i] = -1;
dist[hospital] = 0;
queue<int> q;
q.push(hospital);
ll sum = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
sum += population[u] * dist[u]; // 住在 u 的居民各走 dist[u] 步
for (int v : tree[u]) {
if (dist[v] != -1) continue; // 树中路径唯一,访问过就不再走
dist[v] = dist[u] + 1;
q.push(v);
}
}
return sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (ll u = 1; u <= n; u++) {
ll w, left_child, right_child;
cin >> w >> left_child >> right_child;
population[u] = w;
// 0 表示没有这条链接;链接是双向的,两端都要记
if (left_child != 0) {
tree[u].push_back(left_child);
tree[left_child].push_back(u);
}
if (right_child != 0) {
tree[u].push_back(right_child);
tree[right_child].push_back(u);
}
}
for (ll hospital = 1; hospital <= n; hospital++) {
answer = min(answer, bfs_sum(hospital));
}
cout << answer << "\n";
return 0;
}Python 版是同一个算法:邻接表用列表推导式建好,total_distance 是单源 BFS 返回加权和,min(map(total_distance, range(1, n + 1))) 直接聚合出答案。
import sys
from collections import deque
input = sys.stdin.buffer.readline
n = int(input())
population = [0] * (n + 1)
tree = [[] for _ in range(n + 1)]
for node in range(1, n + 1):
weight, left, right = map(int, input().split())
population[node] = weight
for child in (left, right):
if child:
tree[node].append(child)
tree[child].append(node)
def total_distance(start):
distance = [-1] * (n + 1)
distance[start] = 0
queue = deque([start])
total = 0
while queue:
node = queue.popleft()
total += population[node] * distance[node]
for neighbor in tree[node]:
if distance[neighbor] == -1:
distance[neighbor] = distance[node] + 1
queue.append(neighbor)
return total
print(min(map(total_distance, range(1, n + 1))))拿样例自己对一遍:医院放在 1 号是
两点 Python 笔记:deque 加距离列表做单源 BFS(/home/rainboy/mycode/hugo-blog/content/program_language/python/bfs_shortest.md),把树当成无权图;命名函数与 map / min 的组合(/home/rainboy/mycode/hugo-blog/content/program_language/python/map_reduce_filter.md)。
复杂度
- 时间:每个候选点 BFS 一遍
,共 个候选,总 。本题 ,量级是 次操作。 - 空间:邻接表、距离数组、队列都是
。
解法二:加权换根 DP
思路
枚举法的浪费在于:算出“医院在
把树以
:以 为根的子树里居民总数; :全树居民总数; :医院在 时的总路程。
先算根。 一次 DFS 求出每个点的深度,根的代价就是所有人走到根的路程:
再看父子之间怎么挪。 设
用样例核对这张图:
- 住在
子树里的 个居民,原来要绕到 再回 ,现在少走一步,共省 ; - 其余
个居民,原来到 就停了,现在要多跨一条边,共多花 。
两边相抵:
于是第二遍 DFS 从根往下,把每个点的
公式还顺带说明了一件事:只要某个方向的居民超过总人口的一半(
代码
代码里的名字与上面的记号一一对应:subtree_pop 是 total_pop 是 dist_sum 是
/**
* 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:55
* update_at: 2026-10-29 22:55
*/
// main-reroot.cpp:加权换根 DP,O(n)(P1364 医院设置)。
// 第一次 DFS 求出以 1 为根时每个点的子树居民数 subtree_pop[] 和根上的总路程 dist_sum[1],
// 第二次 DFS 沿父子边递推:把医院从 u 挪到儿子 v,v 子树内的居民少走一步、
// 其余居民多走一步,于是 dist_sum[v] = dist_sum[u] + total_pop - 2 * subtree_pop[v]。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 105;
ll n;
vector<int> tree[MAXN]; // 无向邻接表
ll population[MAXN]; // population[u]:节点 u 的居民数
ll subtree_pop[MAXN]; // subtree_pop[u]:以 1 为根时 u 子树内的居民总数
ll dist_sum[MAXN]; // dist_sum[u]:所有居民走到 u 的总路程
ll total_pop; // 全树居民总数
// 第一次 DFS:算出子树居民数,同时把每个居民到根 1 的路程累进 dist_sum[1]。
void dfs_subtree(ll u, ll parent, ll depth) {
subtree_pop[u] = population[u];
dist_sum[1] += population[u] * depth;
for (int v : tree[u]) {
if (v == parent) continue;
dfs_subtree(v, u, depth + 1);
subtree_pop[u] += subtree_pop[v];
}
}
// 第二次 DFS:换根递推,把每个点的总路程都推出来。
void dfs_reroot(ll u, ll parent) {
for (int v : tree[u]) {
if (v == parent) continue;
// 挪一步的代价变化:v 子树内的居民各少走 1 步,其余居民各多走 1 步
dist_sum[v] = dist_sum[u] - subtree_pop[v] + (total_pop - subtree_pop[v]);
dfs_reroot(v, u);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (ll u = 1; u <= n; u++) {
ll w, left_child, right_child;
cin >> w >> left_child >> right_child;
population[u] = w;
total_pop += w;
if (left_child != 0) {
tree[u].push_back(left_child);
tree[left_child].push_back(u);
}
if (right_child != 0) {
tree[u].push_back(right_child);
tree[right_child].push_back(u);
}
}
dfs_subtree(1, 0, 0);
dfs_reroot(1, 0);
// 全部点的总路程都算出来了,扫一遍取最小
ll answer = dist_sum[1];
for (ll u = 2; u <= n; u++) {
answer = min(answer, dist_sum[u]);
}
cout << answer << "\n";
return 0;
}复杂度
- 时间:第一次 DFS 求
与 共 ,第二次 DFS 递推全部 也是 ,扫描取最小 ,总 。 - 空间:邻接表加三个
数组。
复杂度对比
| 解法一(枚举 + BFS) | 解法二(加权换根 DP) | |
|---|---|---|
| 时间复杂度 | ||
| 空间复杂度 | ||
| 树的遍历次数 | 2 遍 | |
| 代码长度 | 更短,只有 BFS 一个套路 | 稍长,要写两遍 DFS 和一个公式 |
| 何时选它 |
总结
这题的答案定义很简单,关键在于“从相邻两个位置的差别里找规律”:
实现上有两点容易踩:
- 总路程用 64 位整数。
、 ,把医院放在链端点这种最坏情形下中间结果约 , int眼看着就要到头,没必要赌。 - 输入给的是“左链接 / 右链接”,存图时两端都要加边;0 表示没有这条链接,要跳过,不能当成节点 0。
如果题目把 brute.cpp 就是干这个的:它用递归 DFS 做与解法一同样的枚举,专门给换根 DP 当对照基准。
