两次 DFS:先根化建父子关系,再后序求每棵子树大小,边费用 = 边权 × |2·子树大小 − n|。
OJ: luogu
题目 ID: P2052
难度:普及
标签:树形 DP子树大小DFS
创建: 2026-07-17 02:00
更新: 2026-10-24 11:00
形式化题目
给定一棵
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
struct Edge {
int u;
int v;
int w;
};
int count_component(int start, int ban_u, int ban_v,
const vector<vector<int>> &g) {
int n = (int)g.size() - 1;
vector<int> vis(n + 1, 0);
stack<int> st;
st.push(start);
vis[start] = 1;
int cnt = 0;
while (!st.empty()) {
int u = st.top();
st.pop();
++cnt;
for (int v : g[u]) {
if ((u == ban_u && v == ban_v) || (u == ban_v && v == ban_u)) {
continue;
}
if (!vis[v]) {
vis[v] = 1;
st.push(v);
}
}
}
return cnt;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<Edge> edges;
vector<vector<int>> g(n + 1);
for (int i = 1; i <= n - 1; ++i) {
int u, v, w;
cin >> u >> v >> w;
edges.push_back({u, v, w});
g[u].push_back(v);
g[v].push_back(u);
}
long long ans = 0;
for (const auto &e : edges) {
// 直接删掉这条边,暴力数出一侧有多少点。
int left_cnt = count_component(e.u, e.u, e.v, g);
int right_cnt = n - left_cnt;
ans += 1LL * e.w * llabs(1LL * left_cnt - right_cnt);
}
cout << ans << '\n';
return 0;
}brute.cpp 对每条边断开后分别 DFS 数出两侧节点数再累加费用,单条边
关键观察:任选 size[u]——断掉这条边后一侧有 size[u] 个节点,另一侧就是
所以只需两次自顶向下的 DFS 就能收尾:
dfs_build(u):以 1 号点为根,先序记下每个点的父亲parent_arr[],把无根树变成父子关系;dfs_calc(u):后序遍历,subtree_size[u]由所有儿子的子树大小累加得到(含自己 1 个点),在儿子返回后立刻用上面的公式结算父子边费用。
邻接表用 vector<edge> g[maxn] 存(树题只有
代码
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-20 11:28
* update_at: 2026-10-24 11:09
*/
// P2052 [NOI2011] 道路修建
// 两次 DFS:dfs_build 把无根树根化(记下每个点的父亲),
// dfs_calc 后序回推子树大小,并累加每条父子边的修建费用。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000000 + 5; // 国家数上限
typedef long long ll;
struct edge {
int to; // 另一端点
ll w; // 道路长度(边权)
};
ll n; // 国家数
vector<edge> g[maxn]; // g[u] 存与 u 直接相连的所有道路
int parent_arr[maxn]; // parent_arr[u] 是 u 在根化后的父亲,根的值为 0
int subtree_size[maxn]; // subtree_size[u] 是以 u 为根的子树点数
ll ans; // 所有道路的修建费用之和
// 第一次 DFS:以 1 号点为根,先序建出父子关系。
void dfs_build(int u) {
for (const edge& e : g[u]) {
if (e.to == parent_arr[u]) continue; // 跳过父亲,避免走回上一步
parent_arr[e.to] = u;
dfs_build(e.to);
}
}
// 第二次 DFS:后序求子树大小。
// 断开父子边 (u, v) 后,v 一侧有 subtree_size[v] 个点,
// u 一侧有 n - subtree_size[v] 个点,
// 这条边的费用 = e.w * |n - 2 * subtree_size[v]|。
void dfs_calc(int u) {
subtree_size[u] = 1; // 只算自己
for (const edge& e : g[u]) {
if (e.to == parent_arr[u]) continue;
dfs_calc(e.to);
subtree_size[u] += subtree_size[e.to];
ans += e.w * llabs(n - 2LL * subtree_size[e.to]);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n - 1; i++) {
int u, v;
ll w;
cin >> u >> v >> w;
g[u].push_back({v, w});
g[v].push_back({u, w});
}
parent_arr[1] = 0; // 1 号点为根
dfs_build(1);
dfs_calc(1);
cout << ans << '\n';
return 0;
}复杂度
- 时间:两次 DFS 各访问每个点、每条边常数次,
。 - 空间:邻接表
条边记录 + 父亲、子树大小数组, 。
总结
"树边分割"类问题通常只需要知道一侧的子树大小,另一侧由总数减去它得到。用根化把无根树变成有向的父子关系,再自底向上回推子树大小,是最直接的
