题解:洛谷 P14076 货物运输

【题目来源】

洛谷:P14076 [GESP202509 六级] 货物运输 - 洛谷

【题目描述】

A 国有 \(n\) 座城市,依次以 \(1,2,…,n\) 编号,其中 \(1\) 号城市为首都。这 \(n\) 座城市由 \(n−1\) 条双向道路连接,第 \(i\) 条道路(\(1≤i<n\))连接编号为 \(u_i,v_i\) 的两座城市,道路长度为 \(l_i\)。任意两座城市间均可通过双向道路到达。

现在 A 国需要从首都向各个城市运送货物。具体来说,满载货物的车队会从首都开出,经过一座城市时将对应的货物送出,因此车队需要经过所有城市。A 国希望你设计一条路线,在从首都出发经过所有城市的前提下,最小化经过的道路长度总和。注意一座城市可以经过多次,车队最后可以不返回首都。

【输入】

第一行,一个正整数 \(n\),表示 A 国的城市数量。

接下来 \(n−1\) 行,每行三个正整数 \(u_i,v_i,l_i\),表示一条双向道路连接编号为 \(u_i,v_i\) 的两座城市,道路长度为 \(l_i\)

【输出】

一行,一个整数,表示你设计的路线所经过的道路长度总和。

【输入样例】

4
1 2 6
1 3 1
3 4 5

【输出样例】

18

【核心思想】

  1. 问题分析:给定一棵以 \(1\) 号城市为根的树,需要从根出发遍历所有节点,可以重复经过节点和边,最后不必返回根。求经过的道路长度总和的最小值。由于必须访问所有节点,每条边至少需要走一次向下,而为了返回继续访问其他分支,大多数边需要走两次(下去再上来)。最后停留的那条路径上的边可以少走一次返回。因此,总路程 = \(2 \times \text{总边权} - \text{从根到终点的路径长度}\)。为了使总路程最小,应选择距离根最远的节点作为终点,即最大化从根出发的路径长度。

  2. 算法选择

    • DFS 遍历树:从根节点 \(1\) 开始深度优先搜索,计算从根出发到任意节点的最长路径长度 maxd
    • 累加边权:在读入边时累加所有边的权值得到 sum
    • 最终答案2 * sum - maxd
  3. 关键步骤

    • 读入 \(n\),建立邻接表存储无向树。
    • 读入 \(n-1\) 条边,累加边权到 sum
    • 从节点 \(1\) 开始 DFS,传入当前节点 \(u\) 和父节点 \(fa\)
      • 初始化 maxs = 0 表示从 \(u\) 向下走的最长路径。
      • 遍历所有邻接点 \(v\)(跳过 \(fa\)),递归计算 dfs(v, u) + w,更新 maxs
      • 更新全局最长路径 maxd = max(maxd, maxs)
      • 返回 maxs
    • 输出 2 * sum - maxd
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),每个节点和边访问一次。
    • 空间复杂度:\(O(n)\),邻接表存储树。
  5. DFS 遍历树的核心思想

    • 遍历所有节点的最小代价:在树上遍历所有节点并返回起点,每条边必须走两次,总代价为 \(2 \times \text{总边权}\)
    • 允许不返回起点:最后停留的路径可以省去返回的重复走,因此只需减去从起点到终点的路径长度。
    • 最远终点选择:为了使总代价最小,应选择距离起点最远的节点作为终点,即最大化减去的路径长度。因此问题转化为求从根出发的最长路径。
    • 适用于树上遍历、邮差问题等经典场景。

【算法标签】

普及- #DFS-树

【代码详解】

#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用长整型防止溢出
const int N = 100005;  // 定义最大节点数

typedef pair<int, int> PII;  // 定义边类型,存储邻接节点和边权

int n;                      // 树的节点数
int sum = 0;                // 存储所有边的总权值
int ans = 0;                // 存储树的直径(最长路径长度)
vector<PII> ve[N];          // 邻接表存储树结构

/**
 * 深度优先搜索计算从节点u出发的最长路径
 * @param u 当前节点
 * @param fa 父节点(防止回退)
 * @return 从u出发的最长路径长度
 */
int dfs(int u, int fa)
{
    int maxs = 0;  // 存储从u出发的最长路径长度

    // 遍历所有邻接节点
    for (int i = 0; i < ve[u].size(); i++)
    {
        int v = ve[u][i].first;   // 邻接节点
        int l = ve[u][i].second;  // 边权

        // 跳过父节点
        if (v == fa)
            continue;

        // 递归计算子节点的最长路径,并更新当前最大值
        maxs = max(maxs, dfs(v, u) + l);
    }

    // 更新全局最长路径(树的直径)
    ans = max(maxs, ans);

    return maxs;
}

signed main()
{
    // 输入节点数
    cin >> n;

    // 输入树的结构(n-1条边)
    for (int i = 1; i < n; i++)
    {
        int u, v, l;
        cin >> u >> v >> l;

        // 添加到邻接表(无向图)
        ve[u].push_back({v, l});
        ve[v].push_back({u, l});

        // 累加所有边的权值
        sum += l;
    }

    // 计算树的直径(最长路径)
    int t = dfs(1, -1);

    /**
     * 输出结果:
     * 所有边权之和的两倍减去最长路径长度
     * 这是解决"邮差问题"(遍历所有边的最短路径)的经典公式
     */
    cout << sum * 2 - t << endl;

    return 0;
}

【运行结果】

4
1 2 6
1 3 1
3 4 5
18
posted @ 2026-09-13 18:25  团爸讲算法  阅读(5)  评论(0)    收藏  举报