题解:洛谷 P1550 Watering Hole

【题目来源】

洛谷:P1550 [USACO08OCT] Watering Hole G - 洛谷

【题目描述】

Farmer John 的农场缺水了。

他决定将水引入到他的 \(n\) 个田地。他准备通过挖若干井,并在各块田中修筑水道来连通各块田地以供水。在第 \(i\) 号田中挖一口井需要花费 \(W_i\) 元。连接 \(i\) 号田与 \(j\) 号田需要 \(P_{i,j}\)\(P_{j,i}=P_{i,j}\))元。

请求出 FJ 需要为使所有田地都与有水的田地相连或拥有水井所需要的最少钱数。

【输入】

第一行为一个整数 \(n\)

接下来 \(n\) 行,每行一个整数 \(W_i\)

接下来 \(n\) 行,每行 \(n\) 个整数,第 \(i\) 行的第 \(j\) 个数表示连接 \(i\) 号田和 \(j\) 号田需要的费用 \(P_{i,j}\)

【输出】

输出最小开销。

【输入样例】

4
5
4
4
3
0 2 2 2
2 0 3 3
2 3 0 4
2 3 4 0

【输出样例】

9

【核心思想】

  1. 问题分析:给定 \(n\) 个田地,每个田地挖井花费 \(W_i\),田地 \(i\)\(j\) 之间修水道花费 \(P_{i,j}\)。要求所有田地都有水(要么自己挖井,要么通过水道连接到其他有水的田地),求最小总花费。本质上是最小生成树(MST)建模问题:将"挖井"转化为连接虚拟水源节点的边,然后求整个图的最小生成树。

  2. 算法选择

    • 虚拟节点转化:引入虚拟节点 \(0\),从 \(0\)\(i\) 的边权为 \(W_i\)(表示在 \(i\) 挖井),这样每个节点选择"挖井"或"接水道"统一为选边问题
    • Kruskal 算法:用并查集维护连通分量,按边权从小到大选边,保证无环且总权值最小
  3. 关键步骤

    • 读取数据:读入 \(n\)\(n\)\(W_i\),以及 \(n \times n\)\(P\) 矩阵
    • 建图
      • 添加虚拟边:\((0, i, W_i)\)\((i, 0, W_i)\)\(i = 1\)\(n\)
      • 添加田地间边:\((i, j, P_{i,j})\)\((j, i, P_{i,j})\)\(i, j = 1\)\(n\)
    • Kruskal
      • 所有边按权值升序排序
      • 初始化并查集,\(res = 0\)\(cnt = 0\)
      • 遍历每条边,若两端点不在同一集合,合并并累加权值
      • 若最终 \(cnt < n\)(需要 \(n\) 条边连接 \(n+1\) 个节点),返回 \(\infty\)
    • 输出最小生成树总权值
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n^2 \log n^2) = O(n^2 \log n)\),边数 \(O(n^2)\),排序主导
    • 空间复杂度:\(O(n^2)\),存储所有边
  5. 虚拟节点 + Kruskal 的核心思想

    • 挖井与水道的统一:挖井可视为从"水源"引水到该田地,虚拟节点 \(0\) 就是这个公共水源,\(W_i\) 是引水的代价
    • 最小生成树的完备性:MST 保证选出的边集连通所有节点且无环,总权值最小,恰好对应"每个田地有且仅有一种方式获得水"
    • 并查集的路径压缩find 操作带路径压缩,保证近乎 \(O(1)\) 的查询效率
    • 重边处理:输入矩阵包含 \(P_{i,i} = 0\) 的自环,Kruskal 中自环两端点相同自然跳过,无需特殊处理
    • 适用于设施选址、网络建设、带固定成本连接类问题

【算法标签】

普及 #生成树

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 305, M = 2 * 100005, INF = 1e9;
int n, cur;  // n: 节点数, cur: 边计数器
int h[N], e[M], w[M], ne[M], idx;  // 邻接表
int p[N];  // 并查集

struct Edge
{
    int u, v, cost;  // 起点u,终点v顶点编号,cost,边u-v的权值
    bool operator < (const Edge &E) const  // 重载<操作符,按cost从小到大排序
    {
        return cost < E.cost;
    }
} edges[M];

int find(int x)  // 并查集查找
{
    if (p[x] != x)
    {
        p[x] = find(p[x]);
    }
    return p[x];
}
int kruskal()  // Kruskal算法求最小生成树
{
    sort(edges + 1, edges + 1 + cur);  // 边按权值排序
    for (int i = 1; i <= n; i++)  // 初始化并查集
    {
        p[i] = i;
    }
    int res = 0, cnt = 0;  // res: 总权值, cnt: 已选边数
    for (int i = 1; i <= cur; i++)  // 遍历所有边
    {
        int u = edges[i].u, v = edges[i].v, w = edges[i].cost;
        u = find(u), v = find(v);
        if (u != v)  // 不连通,加入生成树
        {
            p[u] = v;
            res += w;
            cnt++;
        }
    }
    if (cnt < n - 1)  // 如果边数不足n-1,图不连通
    {
        return INF;
    }
    return res;  // 返回最小生成树权值
}

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

    memset(h, -1, sizeof(h));
    for (int i = 1; i <= n; i++)  // 添加虚拟节点0的边
    {
        int x;
        cin >> x;
        edges[++cur] = {0, i, x};  // 节点0到节点i
        edges[++cur] = {i, 0, x};  // 节点i到节点0
    }
    for (int i = 1; i <= n; i++)  // 添加节点间的边
    {
        for (int j = 1; j <= n; j++)
        {
            int x;
            cin >> x;
            edges[++cur] = {i, j, x};  // 节点i到节点j
            edges[++cur] = {j, i, x};  // 节点j到节点i
        }
    }

    cout << kruskal();  // 输出最小生成树权值
    return 0;
}

【运行结果】

4
5
4
4
3
0 2 2 2
2 0 3 3
2 3 0 4
2 3 4 0
9
posted @ 2026-08-15 19:48  团爸讲算法  阅读(13)  评论(0)    收藏  举报