题解:洛谷 P8900 Barn Tree

【题目来源】

洛谷:P8900 [USACO22DEC] Barn Tree S - 洛谷

【题目描述】

Farmer John 的农场有 \(N\) 个牛棚 \((2\le N\le 2\times 10^5)\),编号为 \(1\dots N\)。有 \(N-1\) 条道路,每条道路连接两个牛棚,并且从任一牛棚均可通过一些道路到达任一其他牛棚。目前,第 \(j\) 个牛棚中有 \(h_j\) 个干草捆 \((1\le h_j\le 10^9)\)

为使他的奶牛们满意,Farmer John 想移动这些干草,使得每个牛棚都有相同数量的干草捆。他可以选择任何一对由一条道路连接的牛棚,并命令他的农场工人将不超过第一个牛棚中干草捆数量的任意正整数个干草捆从第一个牛棚移动到第二个牛棚。

请求出一个 Farmer John 可以发出的命令序列,以尽可能少的命令数完成这一任务。输入保证存在符合要求的命令序列。

【输入】

输入的第一行包含整数 \(N\)

第二行包含空格分隔的整数 \(h_j\),其中 \(j=1\dots N\)

最后 \(N-1\) 行每行包含两个空格分隔的牛棚编号 \(u_i,v_i\),表示有一条双向道路连接 \(u_i\)\(v_i\)

【输出】

输出命令的最小数量,然后输出该数量的命令序列,每行输出一条命令。

每条命令的格式应为三个空格分隔的正整数:出发牛棚,目标牛棚,以及从出发牛棚移动到目标牛棚的干草捆数量。

如果有多组解,输出任意一组。

【输入样例】

4
2 1 4 5
1 2
2 3
2 4

【输出样例】

3
3 2 1
4 2 2
2 1 1

【核心思想】

  1. 问题分析:给定一棵 \(N\) 个节点的树,每个节点有 \(h_j\) 个干草捆,要求通过最少的边传输命令使每个节点干草数相等(均为平均值)。每次命令沿一条边传输任意正整数个干草捆。本质上是树形 DP + 两次 DFS问题:先自底向上处理过剩节点向上传输,再自顶向下处理不足节点从父节点接收。

  2. 算法选择

    • 差值转化\(h_i \leftarrow h_i - average\),正数表示过剩需传出,负数表示不足需传入
    • 两次 DFSdfs1 自底向上,子节点过剩则向父节点传输;dfs2 自顶向下,父节点向不足子节点传输
  3. 关键步骤

    • 读取数据:读入 \(N\)\(h[1..N]\),计算平均值 \(average = sum / N\)
    • 差值处理\(h[i] \leftarrow h[i] - average\)
    • 建图:读入 \(N-1\) 条边,链式前向星存储
    • dfs1(u, fa) 自底向上
      • 遍历子节点 \(v\)
      • 递归 dfs1(v, u)
      • \(h[v] > 0\)(子节点过剩),记录命令 \((v, u, h[v])\)\(cnt \leftarrow cnt + 1\)
      • \(h[u] \leftarrow h[u] + h[v]\)(父节点累加子节点差值)
    • dfs2(u, fa) 自顶向下
      • 遍历子节点 \(v\)
      • \(h[v] < 0\)(子节点不足),记录命令 \((u, v, -h[v])\)\(cnt \leftarrow cnt + 1\)
      • 递归 dfs2(v, u)
    • 输出命令数 \(cnt\) 和所有命令
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N)\),两次 DFS 各遍历树一次
    • 空间复杂度:\(O(N)\),链式前向星和答案数组
  5. 树形流的核心思想

    • 流量守恒:每个节点的差值 \(h_i - average\) 之和为 \(0\),整棵树构成一个闭合流网络
    • 边的唯一性:树无环,任意两点间路径唯一,每条边只需一次传输即可平衡两端子树
    • 两次遍历的完备性:自底向上处理"子树内部可自给自足"的过剩,自顶向下处理"需要外部补给"的不足,保证每条边最多两次操作(一上一下)
    • 最少命令数:每条边最多需要一次上传和一次下传,两次 DFS 恰好产生最优解
    • 适用于树平衡、资源分配、网络流简化类问题

【解题思路】

【算法标签】

普及+ #DFS-图

【代码详解】

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxN = 2e5+5;
int h[maxN];
struct Edge {  // 链式前向星模板
    int from, to, Next;
}edge[maxN*2];
int head[maxN], eID ;  // 链式前向星模板(head表示每个顶点指向的第一条边,eID为边的编号)

void addEdge(int u, int v)  // 链式前向星模板(参考学习视频 https://www.bilibili.com/video/BV1n94y1u7s2/?spm_id_from=333.337.search-card.all.click&vd_source=82a4c0d754736e1e4be312ad51239c29 )
{
    edge[++eID].from = u;
    edge[eID].to = v;
    edge[eID].Next = head[u];
    head[u] = eID;
}
struct answer {  //
    int from, to, w;
}ans[maxN];
int cnt;
void dfs1(int u, int fa)  // 从下往上贡献
{
    for (int eID = head[u]; eID; eID = edge[eID].Next) {  // 链式前向星模板板
        int v = edge[eID].to;
        if (v==fa) continue;
        dfs1(v, u);
        if (h[v]>0) {  // 如果子节点值大于0,就要向上贡献
            ans[++cnt].from = v;  // from为子节点
            ans[cnt].to = u;  // to为父节点
            ans[cnt].w = h[v];  // 存的干草为子节点的干草数
        }
        h[u] += h[v];  // 无论子节点是正负,父节点都要更新干草数
    }
}
void dfs2(int u, int fa)  // 从上往下传
{
    for (int eID = head[u]; eID; eID = edge[eID].Next) {  // 链式前向星模板板
        int v = edge[eID].to;
        if (v==fa) continue;

        if (h[v]<0) {  // 只有子节点为负值才传
            ans[++cnt].from = u;  // 从父节点
            ans[cnt].to = v;  // 传给子节点
            ans[cnt].w = -h[v];  // 要传的值为负值,所以取负号(用于输出)
        }
        dfs2(v, u);
    }
}
signed main()
{
    int n;
    cin >> n;  // 输入n个牛棚

    int sum = 0;
    for (int i=1; i<=n; i++) {  // 输入n个牛棚的干草数量
        cin >> h[i];
        sum += h[i];  // 并计算总和
    }
    int average = sum / n;  // 计算平均值
    for (int i=1; i<=n; i++) {  // 将牛棚的干草数量减去平均值
        h[i] -= average;
    }
    for (int e=1; e<n; e++) {  // 输入n-1条边
        int u, v;
        cin >> u >> v;
        addEdge(u, v);  // 使用链式前向星数据结构构建图
        addEdge(v, u);
    }
    dfs1(1, 0);
    dfs2(1, 0);
    cout << cnt << endl;
    for (int i=1; i<=cnt; i++) {
        cout << ans[i].from << " " << ans[i].to << " " << ans[i].w << endl;
    }
    return 0;
}

【运行结果】

4
2 1 4 5
1 2
2 3
2 4
3
4 2 2
3 2 1
2 1 1
posted @ 2026-08-17 22:48  团爸讲算法  阅读(7)  评论(0)    收藏  举报