某地区经过对城镇交通状况的调查,得到现有城镇间快速道路的统计数据,并提出“畅通工程”的目标:使整个地区任何两个城镇间都可以实现快速交通(但不一定有直接的快速道路相连,只要互相间接通过快速路可达即可)。现得到城镇道路统计表,表中列出了任意两城镇间修建快速路的费用,以及该道路是否已经修通的状态。现请你编写程序,计算出全地区畅通需要的最低成本。
输入格式:
输入的第一行给出村庄数目N (1≤N≤100);随后的N(N−1)/2行对应村庄间道路的成本及修建状态:每行给出4个正整数,分别是两个村庄的编号(从1编号到N),此两村庄间道路的成本,以及修建状态 — 1表示已建,0表示未建。
输出格式:
输出全省畅通需要的最低成本。
总体介绍prim算法和Kruskal算法:
prim算法:从图的节点上入手,先任意取一个节点,将其标记为已访问,并且将计算出其他节点到该点的“初始”距离,注意,将该节点到自己的距离赋值为0,这样一来就可以在刚开始时找出来距离选定的点(往后称其为源点)最近的点,找到后将该点再次设为已访问,注意,每一次找到一个新的点后,都要判断新的点的加入会不会对还未曾加入的点到已经加入的点的最短距离(也就是这里的最小花费)产生影响,比如原来2到1的花费是3,但是当以1为源点,假设下一步路径找到了4(也就是此时1->4是最短边),而4的加入恰好有(1->2的距离大于4->2的距离),所以此时就应该要(4->2)而不要1->2,然后找(节点数-1)次(因为n个节点构成树需要n-1条边);从这里可以看出,该算法的时间复杂度和节点个数有关,也就是该算法适用于节点数少,边多的情况
Kruskal算法:从各个节点所连成的边出发,该算法原理简单清晰,但是代码实现较为复杂,通俗的说就是把节点放在那里,往里面不断加边(注意,这里的边是按从小到大的顺序排好的),也就是不断加入当前剩余的边中的最小边,但是这样会存在问题,prim算法可以通过设置数组来区分已访问和未访问从而达到再选边时无法形成回路的目的,但是丛边入手的话,边是由两个端点构成的,不能单纯地添加一个新边就将其两个端点全都标记为已访问,这样就达不到连通的目的了,或者说笔者水平有限,目前没有其他的方法来实现,这里,就涉及了判断回路的并查集方法,初始时将每个节点的父节点都设为自身,每添加一个边,如果这条边的两个端点的祖先不相等(构不成回路)就添加这条边,并且将该边起点的父节点设为该边的终点,(注意这里一定要保持一致,即设起点的父节点为终点就要一直保持这个模式,不要一次向左是父节点,一次向右是父节点,这样会导致判断祖先出错),否则丢弃这条边,计入的边将其权值加入最后的结果中即可。从这里也可以看出,该算法重点在看边数多少,因此其适用于边多节点少的图。
下面就是代码部分:
//prim算法
#include <iostream>
#include <cstring>
#include <algorithm>
#include<vector>
#define maxm 63332
using namespace std;
struct path
{
int have_cur;
int v;//表示花费
} G[105][105];
int N;
int visited[105];//访问标记数组
int d[105];//表示当前选点状态下其他未选点到已选点的最短距离,注意每添加一个点都可能对其造成影响
int prim()
{
memset(visited,0,sizeof(visited));
memset(d,maxm,sizeof(d));
d[1]=0;
visited[1]=1;
for(int i=1; i<=N; i++)
d[i]=G[1][i].v;//更新初始时与1号距离最近的值
int cost=0;
int x=1;
while(x<N)
{
int idx=0;
int minm=maxm;
for(int i=1; i<=N; i++)
{
if(!visited[i]&&(d[i]<minm))
{
minm=d[i];//先找到与1号节点距离最近的值
idx=i;
}
}
if(idx==0)
return 0;
visited[idx]=1;
cost+=d[idx];
//更新因选入idx号节点带来的最短距离的变化值
for(int j=1;j<=N;j++)
{
if(!visited[j]&&G[idx][j].v<d[j])
d[j]=G[idx][j].v;
}
x++;
}
return cost;
}
int main()
{
cin>>N;
memset(G,maxm,sizeof(G));
for(int i=0; i<(N*(N-1))/2; i++)
{
int s,d,cos,have_cur;
cin>>s>>d>>cos>>have_cur;
if(have_cur)
{
G[s][d].v=0;
G[d][s].v=0;
}
else
{
G[s][d].v=cos;
G[d][s].v=cos;
}
}
cout<<prim()<<endl;
return 0;
}
//Kruskal算法
#include <iostream>
#include <cstring>
#include <algorithm>
#include<vector>
#define maxm 63332
using namespace std;
//重新定义结构体
struct node{
int s;//左端点
int d;//右端点
int v;//边的权值
}edge[5005];
int N;//节点数
int m;//边的条数
int father[105];//保存i节点的父节点
bool cmp(node a,node b)
{
return a.v<b.v;
}
//这里需要用到并查集来判断是否存在回路
int find(int x)
{
if(x==father[x])//x的祖先为他自己,退出
return x;
//如果x的父节点不是他自己,那么递归查找其祖先
return find(father[x]);
}
int Kruskal()
{
int cost=0;
int num_ed=0;
for(int i=1;i<=N;i++)
father[i]=i;//先让节点的父节点初始化为自己
for(int i=0;i<m;i++)//此时的edge已经按边的权值大小排好序
{
int fs=find(edge[i].s);
int fd=find(edge[i].d);
if(fs!=fd)//因为其祖先不相等,所以不能构成回路,此条边合法,计入cost中
{
father[fs]=fd;
cost+=edge[i].v;
num_ed++;
if(num_ed==N-1)
break;//寻找完毕,(即已经找到了n-1条边)
}
}
return cost;
}
int main()
{
cin>>N;
m=(N*(N-1))/2;
for(int i=0;i<m;i++)
{
int ss,dd,have_cur,v;
cin>>ss>>dd>>v>>have_cur;
edge[i].s=ss;
edge[i].d=dd;
if(!have_cur)
edge[i].v=v;
else
edge[i].v=0;
}
sort(edge,edge+m,cmp);//将边的权值按从小到大排序
cout<<Kruskal()<<endl;
return 0;
}
对于该题目的一些补充:
1.对于已经修建好的道路,不需要考虑是否会对最小生成树的结果造成影响,可以将已经建好的道路的边的权值设为0,再寻找最小生成树即可。
2.prim算法和Kruskal算法一个侧重于点,一个侧重于边,可以适当改变结构体内部元素结构来简化代码量,不必非要共用一套模板。
以上分析如有疏漏,欢迎大佬留言指正!