[NOI2011] 道路修建

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

两次 DFS:先根化建父子关系,再后序求每棵子树大小,边费用 = 边权 × |2·子树大小 − n|。

OJ: luogu

题目 ID: P2052

难度:普及

标签:树形 DP子树大小DFS

创建: 2026-07-17 02:00

更新: 2026-10-24 11:00

形式化题目

给定一棵 nn 个节点的带权无根树。把每条边断开后,树分成两个连通块,这条边的修建费用定义为边权乘以两侧节点数之差的绝对值。求所有边的修建费用之和。

思路

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

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 数出两侧节点数再累加费用,单条边 O(n)O(n),总复杂度 O(n2)O(n^2),只适合小数据。

关键观察:任选 11 号点为根,对于父子边 (fa,u)(fa, u),只需知道 uu 的子树大小 size[u]——断掉这条边后一侧有 size[u] 个节点,另一侧就是 n−size[u]n - \text{size}[u],费用为

w×∣2⋅size[u]−n∣w \times |2 \cdot \text{size}[u] - n|

所以只需两次自顶向下的 DFS 就能收尾:

  1. dfs_build(u):以 1 号点为根,先序记下每个点的父亲 parent_arr[],把无根树变成父子关系;
  2. dfs_calc(u):后序遍历,subtree_size[u] 由所有儿子的子树大小累加得到(含自己 1 个点),在儿子返回后立刻用上面的公式结算父子边费用。

邻接表用 vector<edge> g[maxn] 存(树题只有 n−1n-1 条边、只需遍历不需要边编号,比链式前向星直观);递归深度等于树的深度,n=106n=10^6 的链在判题器 64 MiB 栈下实测通过。

代码

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 各访问每个点、每条边常数次,O(n)O(n)。
  • 空间:邻接表 2(n−1)2(n-1) 条边记录 + 父亲、子树大小数组,O(n)O(n)。

总结

"树边分割"类问题通常只需要知道一侧的子树大小,另一侧由总数减去它得到。用根化把无根树变成有向的父子关系,再自底向上回推子树大小,是最直接的 O(n)O(n) 解法。注意费用和要使用 64 位整数。