题解:洛谷 P1546 Agri-Net

【题目来源】

洛谷:P1546 [USACO3.1] 最短网络 Agri-Net - 洛谷

【题目描述】

FJ 已经给他的农场安排了一条高速的网络线路,他想把这条线路共享给其他农场。为了用最小的消费,他想铺设最短的光纤去连接所有的农场。

你将得到一份各农场之间连接费用的列表,你必须找出能连接所有农场并所用光纤最短的方案。每两个农场间的距离不会超过 \(10^5\)

【输入】

第一行农场的个数 \(N\)\(3 \leq N \leq 100\))。

接下来是一个 \(N \times N\) 的矩阵,表示每个农场之间的距离。理论上,他们是 \(N\) 行,每行由 \(N\) 个用空格分隔的数组成,实际上,由于每行 \(80\) 个字符的限制,因此,某些行会紧接着另一些行。当然,对角线将会是 \(0\),因为不会有线路从第 \(i\) 个农场到它本身。

【输出】

只有一个输出,其中包含连接到每个农场的光纤的最小长度。

【输入样例】

4
0 4 9 21
4 0 8 17
9 8 0 16
21 17 16 0

【输出样例】

28

【核心思想】

  1. 问题分析:给定 \(N\) 个农场以及它们之间两两连接的费用矩阵,需要选择若干条边将所有农场连接成一个连通网络,使得总费用最小。这是一个经典的最小生成树(MST) 问题。对于 \(N \le 100\) 的规模,可以使用 Kruskal 算法(基于并查集)或 Prim 算法求解。

  2. 算法选择

    • Kruskal 算法:将所有边按权值从小到大排序,然后依次尝试加入每条边,如果该边连接的两个顶点当前不在同一个连通分量中,则加入该边,并累计总费用。使用并查集维护连通分量,时间复杂度为 \(O(N^2 \log N)\)(边数最多为 \(N(N-1)/2\)),对于 \(N \le 100\) 非常高效。
    • Prim 算法(备选):从任意顶点开始,逐步扩展生成树,每次选择连接树内与树外顶点的最小权值边,也可解决此问题。
  3. 关键步骤

    • 读入与建图:读入 \(N\) 以及 \(N \times N\) 的费用矩阵。只存储 \(j > i\) 的上三角部分作为边(避免重复),每条边记录起点 x、终点 y 和权值 dis
    • 排序:将所有边按权值从小到大排序。
    • 并查集初始化fa[i] = i,每个顶点自成一个集合。
    • Kruskal 核心循环:遍历排序后的每条边:
      • 查找 xy 的根节点(find 函数)。
      • 若根不同,则合并两个集合(fa[fx] = fy),并将该边权值累加到答案 ans 中。
      • 若根相同,则跳过该边(避免形成环)。
    • 输出:最终 ans 即为最小生成树的总权值。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N^2 \log N)\),主要来自排序,\(N \le 100\),完全可行。
    • 空间复杂度:\(O(N^2)\),用于存储边集。
  5. 最小生成树的核心思想

    • 贪心选择:Kruskal 算法基于贪心策略——每次选择当前权值最小的边,如果加入后不形成环,则将其纳入生成树。这种贪心能保证最终得到全局最优解(MST 的割性质和回路性质)。
    • 并查集的高效性:并查集能够近乎 \(O(1)\) 地判断两个顶点是否连通,并高效合并连通分量,是 Kruskal 算法的理想搭档。
    • 图的存储:对于稠密图(如本题的完全图),存储所有边是合适的;对于稀疏图,则更适合用邻接表。
    • 适用场景:最小生成树是图论中的基础问题,适用于网络设计、线路规划等成本最小化场景。

【解题思路】

【算法标签】

普及 #并查集

【代码详解】

#include <bits/stdc++.h>
using namespace std;
int n, mark=0, ans=0;
int fa[105];  // 并查集数组,存放每个数的祖先
struct node {  // 定义结构体
    int x, y, dis;  // 起始位置、结束位置、距离
}p[10005];  // p数组范围为100*100
bool cmp(node x, node y) {  // 定义排序函数,按照dis从小到大排序
    return x.dis < y.dis;
}
int find(int x)  // 定义并查集查找函数
{
    return fa[x]==x ? x : fa[x]=find(fa[x]);  // 使用三目运算符简化代码
}
int main()
{
    cin >> n;  // 输入n
    for (int i=1; i<=n; i++) {  // 并查集初始化,所有节点的祖先节点都指向自己
        fa[i] = i;
    }
    for (int i=1; i<=n; i++) {  // 双重for循环遍历矩阵
        for (int j=1; j<=n; j++) {
            int tmp;  // 定义临时变量并接受输入
            cin >> tmp;
            if (j>i) {  // 只记录上半部数据
                mark++;  // p数组下标自增1
                p[mark].x = i;  // 记录p[i]的起始位置
                p[mark].y = j;  // 记录p[i]的结束位置
                p[mark].dis = tmp;  // 记录两个位置之间的距离
            }
        }
    }
    sort(p+1, p+mark+1, cmp);  // 按照距离从小到大进行排序
    for (int i=1; i<=mark; i++) {  // for循环遍历p数组
        int x = find(p[i].x);  // 找到x的祖先
        int y = find(p[i].y);  // 找到y的祖先
        if (x!=y) {  // 如果两者的祖先不一样,说明不是同一集合的,需要合并
            fa[x] = y;  // 将x的祖先指向y的祖先(即合并)
            ans += p[i].dis;  // 将两者的距离加到ans中
        }
    }
    cout << ans << endl;  // 最后打印ans
    return 0;
}

【运行结果】

4
0 4 9 21
4 0 8 17
9 8 0 16
21 17 16 0
28
posted @ 2026-08-30 09:18  团爸讲算法  阅读(12)  评论(0)    收藏  举报