[NOI2011] 道路修建
任选根 DFS 求每棵子树大小,边费用 = 边权 × |2·子树大小 − n|,一次遍历累加总费用。
OJ: luogu
题目 ID: P2052
难度:普及
标签:树形 DP子树大小前向星
日期: 2026-07-17 02: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(前向星存边,避免深递归爆栈)得到遍历顺序,逆序回推每个节点的子树大小,同时累加每条父边的费用,整棵树只扫一遍。
代码
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-08-20 11:28
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using Edge = struct { int to; ll w; }; // 树边:to 是另一端点,w 是边权
using Graph = std::vector<Edge>;
const int MAXN = 1000000 + 5; // 节点数上限
int n; // 国家数
Graph tree[MAXN]; // 全局邻接表数组:直接向 tree[u] 加带权边
int parent_arr[MAXN]; // parent_arr[u] 表示 u 在根化后的父亲节点
int parent_w[MAXN]; // parent_w[u] 表示 u 与其父亲的连边权值
int subtree_size[MAXN]; // subtree_size[u] 表示以 u 为根的子树大小
int order_arr[MAXN]; // BFS 得到的节点访问顺序
int order_cnt; // 访问过的节点数
// 从任意点 s 出发 BFS 建父子关系与访问顺序。
// BFS 是队列迭代遍历,n 达到 1e6 时也不会深递归爆栈。
void bfs_build(int s) {
queue<int> q;
q.push(s);
parent_arr[s] = 0; // 根没有父亲
order_cnt = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
order_arr[++order_cnt] = u;
for (Edge e : tree[u]) {
int v = e.to;
if (v == parent_arr[u]) {
continue; // 跳过父亲方向,避免走回上一步
}
parent_arr[v] = u;
parent_w[v] = e.w;
q.push(v);
}
}
}
// 逆序遍历顺序回推子树大小,同时累加每条父子边的修建费用。
// 断开父子边 (fa, u) 后,u 一侧有 subtree_size[u] 个节点,
// 另一侧为 n - subtree_size[u],费用 = 边权 * |2*子树大小 - n|。
ll calc_cost() {
ll ans = 0;
for (int i = 1; i <= n; i++) {
subtree_size[i] = 1;
}
// BFS 顺序中子节点一定在父节点之后,
// 从后往前即可保证算完每个 u 时子树大小已完整。
for (int i = order_cnt; i >= 2; i--) {
int u = order_arr[i];
ll diff = llabs(1LL * n - 2LL * subtree_size[u]);
ans += 1LL * parent_w[u] * diff;
subtree_size[parent_arr[u]] += subtree_size[u];
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n - 1; i++) {
int u, v, w;
cin >> u >> v >> w;
tree[u].push_back({v, w});
tree[v].push_back({u, w});
}
bfs_build(1);
cout << calc_cost() << '\n';
return 0;
}复杂度
- 时间:一次遍历 + 逆序汇总,
。 - 空间:前向星与子树大小数组,
。
总结
"树边分割"类问题通常只需要知道一侧的子树大小,另一侧由总数减去它得到。用根化把无根树变成有向的父子关系,再自底向上回推子树大小,是最直接的
