前言:
最小生成树:一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。
假设上面给的图转换成现实例子:
上面图中的椭圆代表着一个城镇,椭圆中的数字代表城镇的编号。每两个城镇之间的横线代表着这两个城镇的路,横线上的数字代表着这条路的长度。
问你,想在这些城镇之间修建道路,怎样修建才能使所有的城镇两两连通,并且修路的距离最短?
给出数据:
6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2
把给出的数据变成关系矩阵:
思路:
1.prim算法
把城镇分成两个集合:E()代表这个集合中的城镇可以连通;V()代表这个集合中的城镇还未连通;
我们随便选取一个城镇,加入集合E(),然后每一次都是在集合V()中选取一个离集合E()中的任意一个城镇的距离最近的城镇加入集合E()中;一直到把所有的城镇都连通(即进行N-1);
以上面的图为例子:
最开始两个集合的情况是:E(),V(1,2,3,4,5,6);
(1),我们首先选择1号城镇加入集合E ; {E(1),V(2,3,4,5,6)};
(2)然后和1号城镇相连接的最近的城镇是2号城镇,所以连接2号城镇;{E(1,2),V(3,4,5,6)};
(3)接下来,集合V中和集合E中相连的最小值是1号和2号,所以连接3号城镇;{E(1,2,3),V(4,5,6)};
(4)下面集合V中和集合E中相连的最小值是2号和3号,但是2和3已经连通,所以选择下一个3和4,所以连接4号城镇;
{E(1,2,3,4),V(5,6)};
(5)下面集合V中和集合E中相连的最小值是4号和6号,所以连接6号城镇;{E(1,2,3,4,6),V(5)};
(6)最后,集合V中和集合E中相连的最小值是6号和5号,所以连接5号城镇;{E(1,2,3,4,6,5),V()};
上面就得出了最后的结果图,所以最小生成树的值为:1+2+9+3+4=19;
但是在代码中不易实现,所以就用一个dis数组来记录点到树根的距离,每一次都选取一个到树距离最小的(不是到树根的距离),然后以这个点为中间结点,更新生成树到每一个非树顶点的距离;
代码如下:
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int N=101;
const int INF=99999999;
int n,m;
int map[N][N];//存储地图;
int book[N];//数组标记这个点是否被加入最小生成树中;
int dis[N];//标记该点到树根的距离;
int countt,sum,minn,u;
void init()//数据初始化;
{
countt=0;
sum=0;
memset(book,0,sizeof book);
for(int i=1; i<=n; i++)
{
for(int j=1; j<=n; j++)
{
if(i==j)
map[i][j]=0;
else
map[i][j]=INF;
}
}
return ;
}
void prim()
{
book[1]=1;//选取节点1加入生成树,成为树根;
countt++;//加入生成树中节点的个数;
while(countt<n)//全部加入生成树中需要进行n-1次的选择;
{
minn=INF;
for(int i=1; i<=n; i++)//每一次都是选取到树根距离最小的点;
{
if(book[i]==0&&dis[i]<minn)
{
minn=dis[i];
u=i;
}
}
book[u]=1;
sum+=dis[u];
countt++;
for(int j=1; j<=n; j++)//以上面选取的点为中间点,开始更新生成树的每一个非树顶点的距离;
{
if(book[j]==0&&dis[j]>map[u][j])
dis[j]=map[u][j];
}
}
printf("%d\n",sum);
return ;
}
int main()
{
scanf("%d%d",&n,&m);
init();
int a,b,c;
for(int i=0; i<m; i++)
{
scanf("%d%d%d",&a,&b,&c);
map[a][b]=map[b][a]=c;
}
for(int i=1; i<=n; i++)
dis[i]=map[1][i];
prim();
return 0;
}
2.Kruskal算法
prim算法,每一次选择的是距离树最近的边加入生成树,Kruskal算法却是选择最短的边,判断要不要加入生成树;
Kruskal算法每一次都是选择一条最短的边,但是在选择的过程中要保证不要形成环路;
(1),首先,最短的边是(1,2),所以选择边(1,2);
(2),下面就要选择边(1,3);
(3),接着选择边(4,6);
(4),接着选择边(5,6);
(5),下面选择边(2,3),但是会构成环路,所以不选;
(6),下面选择边(4,5),但是也会构成环路,所以不选;
(6),最后只能选择边(3,4);
所以最后的结果是:1+2+3+4+9=19;
Kruskal算法运用了并查集的内容,因为要分辨有没有形成环路;
代码如下:
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int N=5000;
struct node//存储每一条边
{
int from;
int to;
int w;
}a[N*10];
bool cmp(node aa,node bb)//排序
{
return aa.w<bb.w;
}
int n,m,k,t;
int f[N];//存储每一个点的父亲节点
int getf(int v)//寻找父亲节点;
{
if(f[v]==v)
return f[v];
else
return f[v]=getf(f[v]);
}
int Kruskal()
{
sort(a,a+k,cmp);//按照边的权值由小到大进行排序;
int ans=0,countt=1;
for(int i=0;i<k;i++)
{
int t1=getf(a[i].from);
int t2=getf(a[i].to);
if(t1!=t2)//父亲节点不同,不会构成环路;
{
ans+=a[i].w;
f[t2]=t1;//把父亲节点改成相同;
countt++;
if(countt==n)//如果加入n条边就可以结束了;
break;
}
}
if(countt==n)
return ans;
else
return -1;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=0;i<k;i++)
{
scanf("%d%d%d",&a[i].from,&a[i].to,&a[i].w);
}
for(int i=0;i<=N;i++)//初始化,自己的父亲节点是自己;
f[i]=i;
int kk=Kruskal();
printf("%d\n",kk);
return 0;
}