题解:洛谷 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
【核心思想】
-
问题分析:给定 \(N\) 个农场以及它们之间两两连接的费用矩阵,需要选择若干条边将所有农场连接成一个连通网络,使得总费用最小。这是一个经典的最小生成树(MST) 问题。对于 \(N \le 100\) 的规模,可以使用 Kruskal 算法(基于并查集)或 Prim 算法求解。
-
算法选择:
- Kruskal 算法:将所有边按权值从小到大排序,然后依次尝试加入每条边,如果该边连接的两个顶点当前不在同一个连通分量中,则加入该边,并累计总费用。使用并查集维护连通分量,时间复杂度为 \(O(N^2 \log N)\)(边数最多为 \(N(N-1)/2\)),对于 \(N \le 100\) 非常高效。
- Prim 算法(备选):从任意顶点开始,逐步扩展生成树,每次选择连接树内与树外顶点的最小权值边,也可解决此问题。
-
关键步骤:
- 读入与建图:读入 \(N\) 以及 \(N \times N\) 的费用矩阵。只存储 \(j > i\) 的上三角部分作为边(避免重复),每条边记录起点
x、终点y和权值dis。 - 排序:将所有边按权值从小到大排序。
- 并查集初始化:
fa[i] = i,每个顶点自成一个集合。 - Kruskal 核心循环:遍历排序后的每条边:
- 查找
x和y的根节点(find函数)。 - 若根不同,则合并两个集合(
fa[fx] = fy),并将该边权值累加到答案ans中。 - 若根相同,则跳过该边(避免形成环)。
- 查找
- 输出:最终
ans即为最小生成树的总权值。
- 读入与建图:读入 \(N\) 以及 \(N \times N\) 的费用矩阵。只存储 \(j > i\) 的上三角部分作为边(避免重复),每条边记录起点
-
时间/空间复杂度:
- 时间复杂度:\(O(N^2 \log N)\),主要来自排序,\(N \le 100\),完全可行。
- 空间复杂度:\(O(N^2)\),用于存储边集。
-
最小生成树的核心思想:
- 贪心选择: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
浙公网安备 33010602011771号