【题目描述】
农民约翰被选为他们镇的镇长!他其中一个竞选承诺就是在镇上建立起互联网,并连接到所有的农场。当然,他需要你的帮助。约翰已经给他的农场安排了一条高速的网络线路,他想把这条线路共享给其他农场。为了用最小的消费,他想铺设最短的光纤去连接所有的农场。你将得到一份各农场之间连接费用的列表,你必须找出能连接所有农场并所用光纤最短的方案。每两个农场间的距离不会超过100000。
【输入】
第一行:农场的个数,N(3≤N≤100)。
第二行..结尾:后来的行包含了一个N×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<=100),且输入格式为邻接矩阵(稠密图)。
我们有三种主流解法:(这里只写两种,堆优化的prim前几篇都有写,想看的可以直接去看我真对洛谷p3366的题解)
-
朴素 Prim 算法 (推荐):
-
原理:以点为核心,每次寻找离集合最近的点。
-
复杂度:O(N^2)。
-
优势 :非常适合稠密图 和邻接矩阵输入。对于N=100,运算量仅10000,效率极高,且代码不需要额外的结构体和排序,最简洁。
-
-
Kruskal 算法:
-
原理:以边为核心,排序后贪心选取不构成环的边。
-
复杂度:O(M log M)。
-
注意:由于输入是矩阵,边数M约等于N^2。虽然排序略耗时,但对于本题的数据规模依然能轻松AC。
-
技巧:在读取矩阵时,可以只读取j>i的部分(右上半矩阵),这样既过滤了自环,又去除了重复边,减少了一半的存储和排序压力。
-
2. 解法一:朴素 Prim 算法 (邻接矩阵)
这是解决本题理论最优 的方法。我们维护 dis 数组表示每个点到当前生成树集合的最短距离。
cpp
//这道题最优选择朴素prim 复杂度O(N^2)
#include <iostream>
#include <cstring>//对应memset
using namespace std;
int n;
int g[110][110];
int dis[110];//每个点到集合的距离
int vis[110];//标记每个点是否已经被连接上(加入集合)
int pre[110];
long long sum;//最小生成树的长度
void prim(int s){
dis[s]=0;//起点到自己距离为0
for(int i=1;i<=n;i++){//n个点需要加入集合
int p=0;
//在未加入集合的农场中找距离集合最近的农场
for(int j=1;j<=n;j++){
if(vis[j]==0 && dis[j]<dis[p])
p=j;
}
//连通性检查
//如果已经不存在可以连通的农场就退出
if(p==0 || dis[p]==0x3f3f3f3f) break;
vis[p]=1;//找到了就标记加入集合
sum+=dis[p];//把p到集合的距离加入最小生成树长度
//用p点去尝试更新所有p的未被点亮的临接点
for(int j=1;j<=n;j++){
//如果该邻接点未加入集合且经过p点到集合的距离小于目前
//自身到集合的距离 就更新该邻接点到集合的距离
if(vis[j]==0 && dis[j]>g[p][j]){
dis[j]=g[p][j];
//j点被p更新到集合的最短距离,前驱就是p
//因为目前来看j经过p到集合距离最短
pre[j]=p;
}
}
}
}
int main(){
cin>>n;
memset(dis,0x3f,sizeof(dis));//初始化每个点到集合的距离为无穷
//邻接矩阵存图
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
//虽然是无向边,但是输入数据为邻接矩阵
//所以不需要再去双向加边
cin>>g[i][j];
}
}
prim(1);//从1号点开始
cout<<sum;
}
3. 解法二:Kruskal 算法 (输入优化版)
使用并查集维护连通性。
关键点:在读取矩阵时,必须先 cin>>x读取数据,然后再判断 if(j>i)决定是否存储。如果直接在if里cin,会导致输入错位(吞掉数据)。
cpp
//kruskal
#include <iostream>
#include <algorithm>//对应sort函数
using namespace std;
struct edge{
int u,v,w;
//按边权从小到大排序
friend bool operator<(edge a,edge b){
return a.w<b.w;
}
};
edge e[10010];//边集数组
edge mst[10010];//记录最小生成树的边(记录网络线路)
int cnt1;//记录边集数组的边数
int cnt2;//记录最小生成树边数
int n;
int fa[110];//记录每个点在集合中的老大
long long sum;//记录最小生成树的总长度(光纤的最小长度)
//并查集+查询
int find(int x){
if(fa[x]==x) return x;//如果已经是根结点,就返回
//否则递归找根节点,并把根节点赋给沿途所有节点
return fa[x]=find(fa[x]);
}
void uni(int a,int b){
int faa=find(a);//找a老大
int fab=find(b);//找b老大
if(faa!=fab){//如果老大相同无事发生
fa[fab]=faa;//不同就让a老大变成b老大的老大
}
}
void kruskal(){
for(int i=1;i<=cnt1;i++){
int a=e[i].u;//第i条边的一个端点
int b=e[i].v;//第i条边的另外一个端点
if(find(a)!=find(b)){//如果这两个端点不在一个集合里(不连通)
cnt2++;//就把这两个端点脸上,然后边数记录+1
sum+=e[i].w;//更新最小生成树长度
mst[cnt2].u=a;
mst[cnt2].v=b;
mst[cnt2].w=e[i].w;
uni(a,b);
}
}
}
int main(){
cin>>n;
//储存边集数组
for(int i=1;i<=n;i++) fa[i]=i;//初始化每个点自成集合(老大是自己)
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
int x;
cin>>x;//第i条边边权
//只存j大于i的边,这样可以少存一半重复边以及自环边(虽然全存进去也没事)
if(j>i){
e[++cnt1].u=i;
e[cnt1].v=j;
e[cnt1].w=x;
}
}
}
//对边集数组按边权从小到大排序
sort(e+1,e+cnt1+1);
kruskal();
cout<<sum;
return 0;
}
4. 总结
-
朴素 Prim:针对N<=100的矩阵输入题,它是最稳健、代码量最少的选择。
-
Kruskal :通用性强,利用
if(j>i)的技巧可以很好地处理矩阵输入,去除冗余边。 -
易错点 :Prim别忘了头文件
#include <cstring>(或者写万能头);Kruskal读取矩阵时一定要先读入再判断。