##前言
文章记录数据结构中求最小生成树的两种经典算法:Kruskal和Prim算法。
##Kruskal算法
算法的实现步骤:
个人理解:
- 将图中所有的带权边按照权值大小(从小到大的次序)依次排序;形如<2,5>:3;<2,3>:4等,表示结点2到结点5能够连通,路径长度为3。
- 从上按照从小到大的‘顶点边权列表’中依次选择边权值最小且不构成环路的这些边形成最小生成树。
老师给的步骤:
- 将所有的边按权值排序;
- 设每个顶点为一个独立的点集,生成树T为空集;
- 依序扫描每一条边,直到已输出n-1条边: 若vi、vj均不在同一点集中,则将该边加入生成树T中,并合并这两个点集;否则舍弃该边;
图示辅助理解:
算法的代码实现:
#include<iostream>
#include<algorithm>
#define maxv 101
#define maxe 100
using namespace std;
int r[maxe];
int p[maxv];
int u[maxv],v[maxv],w[maxv];
int n,m;
int cmp(const int i,const int j)
{
return w[i]<w[j];
}
int find(int x)
{
return p[x]==x?x:p[x]=find(p[x]);
}
int Kruskal(int u[maxv],int v[maxv],int w[maxv],int n,int m)
{
int ans=0;
//循环设置每一个顶点序号
for(int i=0;i<n;i++)
p[i]=i;
for(int i=0;i<m;i++)
r[i]=i;
sort(r,r+m,cmp);
/*
cmp 为sort函数中的排序规则
其中的常量 i j 表示待排序中的两个变量
return 后的内容表示排序的规则
w[i]<w[j] 按照 w 权值的大小进行排序
比较结果为真返回 1 否则返回 0
*/
//排序后 边权
//for(int i=0;i<m;i++)
// cout<<"第"<<r[i]<<"条边的权值为:"<<w[r[i]]<<" "<<endl;
for(int i=0;i<m;i++)
{
int e=r[i];
//x y 分别定义为 e 这条边的起点和终点
//利用递归来判定该边能达到联通终点是否与起点有环
int x=find(u[e]);
int y=find(v[e]);
if(x!=y)
{
ans+=w[e];
cout<<"第"<<e<<"条边:权值为:"<<w[e]<<"; 从" <<u[e]<<"该起点到"<<v[e]<<"该终点"<<endl;
p[x]=y;
}
}
return ans;
}
int main()
{
int n,m,ans;
while(cin>>n>>m)
{
for(int i=0;i<m;i++)
cin>>u[i]>>v[i]>>w[i];
ans=Kruskal(u,v,w,n,m);
cout<<"最小生成树权值最小是:"<<ans<<endl;
}
return 0;
}
/*测试数据
7 12
2 5 3
2 3 4
3 5 5
1 4 7
0 2 9
0 3 10
1 6 12
5 6 32
3 6 23
0 1 22
4 6 20
1 3 15
*/
运行截图:
##Prim算法
算法的实现步骤:
- E1:任取一个顶点构成U={v0};构造向量cost[0…n-1]和adj[0…n-1],cost[i]表示顶点vi到U的最短边的长度,adj[i]表示顶点vi到U的最短边在U中的邻接点的下标;其中,vi∈V-U。初始时,生成树T为空集。
- E2:重复n-1次
- E21:从V-U中选出cost值最小的顶点vk,将边(vk, vadj[k])加入到生成树T中,然后将vk并入U中;
- E22:修正V-U中各顶点的cost值和adj值;
图示辅助理解
算法的代码实现:
#include<iostream>
#define maxv 101
#define maxe 100
#define INF 1000000
using namespace std;
void Prim(int n ,int arc[][101])
{
//定义 ans 为最小生成树最小权值
int cost[maxv],closet[maxv],ans=0;
closet[0]=0;//从 V-U 到 U 连接最短边对应的邻接点序号
cost[0]=0;//将 0元素加入到 U集合中 设置标志数组
for(int i=1;i<n;i++)
{
//初始仅有 0 加入 U cost和closet依次初始化
cost[i]=arc[0][i];
closet[i]=0;
}
for(int i=1;i<n;i++)
{
//从V-U中取出一个元素加入到 U中 共执行 n-1次
int min=INF,k=0;
for(int j=1;j<n;j++)
{
//if条件为判定该结点是否在 U中 且是否为连接 U的权值最小
if(cost[j]&&cost[j]<min)
{
min=cost[j];//更新最小值
k=j;//记录连接 U和 V-U的接点
}
}
//更新 MST权值和
ans+=min;
//将该点加入U集合中
cost[k]=0;
for(int j=1;j<n;j++)
{
//更新cost 和closet值
if(cost[j]&&cost[j]>arc[k][j])
{
cost[j]=arc[k][j];
closet[j]=k;
}
}
}
//输出 最小生成树 权值最小的值
cout<<ans<<endl;
}
int main()
{
int n,arc[maxv][maxv];
cin>>n;
//输入图邻接矩阵
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>arc[i][j];
//调用Prim算法
Prim(n,arc);
return 0;
}
##Prim和Kruskal算法的区别和联系
Prim算法是加入一个顶点到集合U中比较与U中的点相连结点的边的长度,适合用于稠密图即顶点边比较多的情况。
Kruskla算法是将所有的边权和顶点依次罗列排序,进行依次比较,得出最小生成树,比较适合于稀疏图即顶点边比较少的情况。
Prim算法和Kruskal算法均属于贪心算法。贪心算法一般上是在当前情况下求取最优解,可能无法得出‘全局最优解’,会陷入‘局部最优解’的可能。但对于最小生成树来说,Prim和Kruskal算法均得到是最优解。有具体的数学论证。
Prim的算法效率E+VlogV,Kruskal的算法效率ElogE。