kruskal算法
kruskal算法:采用边贪心的策略。
kruskal算法基本思想:在初始状态时隐去图中的所有边,这样图中每个顶点都自成一个连通块。之后执行下面的步骤:
- 对所有边按边权从小到大进行排序
- 按边权从小到大测试所有边,如果当前测试边所连接的两个顶点不在同一个连通块中,则把这条测试边加入当前最小生成树中;否则,将边舍弃。
- 执行步骤2,直到最小生成树中的边数等于总顶点数减1或是测试完所有边时结束。而当结束时如果最小生成树的边数小于总顶点数减1,说明该图不连通。
对下图进行kruskal算法步骤
- 当前图中边权最小的边为V0V4,权值为1。由于V0和V4在不同的连通块中,因此把边V0V4加入最小生成树中。
- 当前图的剩余边中边权最小的边为V1V2,权值为1。由于V1和V2在不同的连通块中,因此把边V1V2加入最小生成树中。
- 当前图的剩余边中边权最小的边为V0V5,权值为2。由于V0和V5在不同的连通块中,因此把边V1V2加入最小生成树中。
- 当前图的剩余边中边权最小的边为V4V5,权值为3。由于V4和V5在同一个连通块中,因此如果加入边V4V5,就会形成一个环,故淘汰。
- 当前图的剩余边中边权最小的边为V1V5,权值为3。由于V1和V5在不同的连通块中,因此把边V1V5加入最小生成树中。
每次选择图中最小边权的边,如果边两端的顶点在不同的连接块中,就把这条边加入最小生成树中。
定义一个结构体,在里面存放边的两个端点编号与边权。
struct edge
{
int u, v; //边的两个端点编号
int cost; //边权
}E[MAXE]; //最多有MAXE条边
写个排序函数来让数组E按边权从小到大排序
bool cmp(edge a, edge b)
{
return a.cost < b.cost;
}
kruskal算法伪代码
int kruskal()
{
令最小生成树的边权之和为ans、最小生成树的当前边数Num_Edge;
将所有边按边权从小到大排序;
for(从小到大枚举所有边)
{
if(当前测试边的两个端点在不同的连通块中)
{
将该测试边加入最小生成树中;
ans += 测试边的边权;
最小生成树的当前边数Num_Edge加1;
当边数Num_Edge等于顶点数减1时结束循环;
}
}
return ans;
}
这伪代码中有两个细节不太直观:
1)如何判断测试边的两个端点是否在不同的连通块中
2)如何将测试边加入最小生成树中
如果把每个连通块当作一个集合,那么就可以把问题转换为判断两个端点是否在同一个集合中——并查集。
并查集可以通过查询两个结点所在集合的根结点是否相同来判断它们是否在同一集合,而合并功能恰好可以把上面提到的第二个细节解决,即只要把测试边的两个端点所在集合合并,就能达到将边加入最小生成树的效果。
int father[N]; //并查集数组
int findFather(int x) //并查集查询函数
{
...
}
//kruskal函数返回最小生成树的边权之和,参数n为顶点个数,m为图的边数
int kruskal(int n, int m)
{
//ans为所求边权之和,Num_Edge为当前生成树的边数
int ans = 0, Num_Edge = 0;
for(int i = 1; i <= n; i++) //假设题目中顶点范围是[1,n]
{
father[i] = i; //并查集初始化
}
sort(E, E + m, cmp); //所有边按边权从小到大排序
for(int i = 0; i < m; i++) //枚举所有边
{
int faU = findFather(E[i].u); //查询测试边两个端点所在集合的根节点
int faV = findFather(E[i].v);
if(faU != faV) //如果不在一个集合中
{
father[faU] = faV; //合并集合(即把测试边加入最小生成树)
ans += E[i].cost; //边权之和增加测试边的边权
Num_Edge++; //当前生成树的边数加1
if(Num_Edge == n - 1)
break; //边数等于顶点数减1时结束算法
}
}
if(Num_Edge != n - 1)
return -1; //无法连通时返回-1
else
return ans; //返回最小生成树的边权之和
}
如果是稠密图(边多),则用prim算法;如果是稀疏图(边少),则用kruskal算法。
对上述图kruskal算法全部实现
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXV = 110;
const int MAXE = 10010;
//边集定义部分
struct edge
{
int u, v; //边的两个端点编号
int cost; //边权
}E[MAXE]; //最多有MAXE条边
bool cmp(edge a, edge b)
{
return a.cost < b.cost;
}
//并查集部分
int father[MAXV]; //并查集数组
int findFather(int x) //并查集查询函数
{
int a = x;
while(x != father[x])
{
x = father[x];
}
//路径压缩
while(a != father[a])
{
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
//kruskal部分,返回最小生成树的边权之和,参数n为顶点个数,m为图的边数
int kruskal(int n, int m)
{
//ans为所求边权之和,Num_Edge为当前生成树的边数
int ans = 0, Num_Edge = 0;
for(int i = 0; i < n; i++) //顶点范围是[0,n-1]
{
father[i] = i; //并查集初始化
}
sort(E, E + m, cmp); //所有边按边权从小到大排序
for(int i = 0; i < m; i++) //枚举所有边
{
int faU = findFather(E[i].u); //查询测试边两个端点所在集合的根结点
int faV = findFather(E[i].v);
if(faU != faV) //如果不在一个集合中
{
father[faU] = faV; //合并集合(即把测试边加入最小生成树中)
ans += E[i].cost; //边权之和增加测试边的边权
Num_Edge++; //当前生成树的边数加1
if(Num_Edge == n - 1)
break; //边数等于顶点数减1时结束算法
}
}
if(Num_Edge != n - 1)
return -1; //无法连通时返回-1
else
return ans; //返回最小生成树的边权之和
}
int main()
{
int n, m;
scanf("%d%d", &n, &m); //顶点数、边数
for(int i = 0; i < m; i++)
{
scanf("%d%d%d", &E[i].u, &E[i].v, &E[i].cost); //两个端点编号、边权
}
int ans = kruskal(n, m); //kruskal算法入口
printf("%d\n", ans);
return 0;
}
//输入数据
6 10 //6个顶点、10条边。下面跟着10行无向边
0 1 4 //0号顶点与1号顶点的无向边的边权为4,下同
0 4 1
0 5 2
1 2 1
1 5 3
2 3 6
2 5 5
3 4 5
3 5 4
4 5 3
//输出结果
11