参考博客:https://blog.csdn.net/YF_Li123/article/details/75195549
最小生成树之kruskal(克鲁斯卡尔)算法
kruskal算法:同样解决最小生成树的问题,和prim算法不同,kruskal算法采用了边贪心的策略,思想要比prim算法简单。
算法基本思想:在初始状态时隐去图中的所有边,这样图中每个顶点都自成一个连通块。之后执行下面的步骤:
(1)对所有的边按边权从小到大进行排序;
(2)按边权从小到大测试所有边,如果当前测试边所连接的两个顶点不在同一个连通块中,则把这条测试边加入当前最小生成树中;否则,将边舍弃;
(3)执行步骤(2),知道最小生成树中的边数等于总顶点数减1或者测试完所有的边时结束。当结束时,如果最小生成树的边数小于总顶点数减1,则说明该图不连通。
执行过程:
下面通过举例来说明kruskal算法的执行流程,:
(1)如图(a),当前图中边权最小的边为(V0,V4),权值为1。由于V0和V4不在同一个连通块中,因此把(V0,V4)加入最小生成树,此时最小生成树中有1条边,权值之和为1。
(2)如图(b),当前图中边权最小的边为(V1,V2),权值为1。由于V1和V2不在同一个连通块中,因此把(V1,V2)加入最小生成树,此时最小生成树中有2条边,权值之和为2。
(3)如图(c),当前图中边权最小的边为(V0,V5),权值为2。由于V0和V5不在同一个连通块中,因此把(V0,V5)加入最小生成树,此时最小生成树中有3条边,权值之和为4。
(4)当前图中边权最小的边为(V4,V5),权值为3。由于V4和V5在同一个连通块中,如果加入就会形成一个环,因此把(V4,V5)舍弃。
(5)如图(d),当前图中边权最小的边为(V1,V5),权值为3。由于V1和V5不在同一个连通块中,因此把(V1,V5)加入最小生成树,此时最小生成树中有4条边,权值之和为7。
(6)当前图中边权最小的边为(V0,V1),权值为4。由于V0和V1在同一个连通块中,如果加入就会形成一个环,因此把(V0,V1)舍弃。
(7)如图(e),当前图中边权最小的边为(V3,V5),权值为4。由于V3和V5不在同一个连通块中,因此把(V3,V5)加入最小生成树,此时最小生成树中有5条边,权值之和为11。
此时由于最小生成树的边数为5,恰好等于定点数减1,因此kruskal算法结束,最后所得的最小生成树的边权之和为11。
注意:本文代码里需要注意两个细节:
(1)如何判断测试边的两个端点是否在不同的连通块中;
(2)如何将测试边加入到最小生成树中;
其实我们可以把每个连通块当做一个集合,判断两个端点是否在同一个连通块中就可以转换为判断两个端点是否在同一个集合中,解决这个问题的方法是使用并查集。并查集可以通过查询两个结点所在集合的根结点是否相同来判断它们是否在同一个集合中,而合并功能恰好可以解决上面提到的第二个问题,即只要把测试边的两个端点所在集合合并,就能达到将边加入最小生成树的目的。
代码:
main.cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 定义边
struct edge
{
int u, v; // 边的两个端点编号
float cost; // 边的权值
edge(int x, int y, float c): u(x), v(y), cost(c) {}
};
// 边的比较函数
bool cmp(edge a, edge b)
{
return a.cost < b.cost;
}
// 并查集查询函数,返回x所在集合的根结点
int findFather(vector<int> father, int x)
{
int a = x;
while (x != father[x])
{
x = father[x];
}
// 更新father参数
int z;
while (a != father[a])
{
z = a;
a = father[a];
father[z] = x;
}
return x;
}
// Kruskal算法求无向图的最小生成树
void Kruskal(int n, int m, vector<edge> &E, vector<edge> &res, float &totalCost)
{
// n:顶点个数
// m:边的个数
// E:边的合集
vector<int> father(n);
int faU, faV;
for (int i = 0; i < n; ++i)
{
father[i] = i;
}
sort(E.begin(), E.end(), cmp);
for (int i = 0; i < m; ++i)
{
faU = findFather(father, E[i].u);
faV = findFather(father, E[i].v);
if (faU != faV)
{
res.push_back(E[i]);
father[faU] = faV;
totalCost += E[i].cost;
// if (NumEdge == n-1) break; // 如果原图是由多个区域组成,则注释这一句
}
}
}
int main()
{
vector<edge> E = {edge(0, 7, 0.16),
edge(2, 3, 0.17),
edge(1, 7, 0.19),
edge(0, 2, 0.26),
edge(5, 7, 0.28),
edge(1, 3, 0.29),
edge(1, 5, 0.32),
edge(2, 7, 0.34),
edge(4, 5, 0.35),
edge(1, 2, 0.36),
edge(4, 7, 0.37),
edge(0, 4, 0.38),
edge(6, 2, 0.40),
edge(3, 6, 0.52),
edge(6, 0, 0.58),
edge(6, 4, 0.93)};
random_shuffle(E.begin(), E.end());
cout << "Original Undirected Graphs: " << endl;
for (size_t i = 0; i < E.size(); ++i)
{
cout << "edge: " << E[i].u << " " << E[i].v << " " << E[i].cost << endl;
}
cout << "----------------" << endl;
int n = 8;
int m = 16;
vector<edge> res;
float totalCost = 0;
Kruskal(n, m, E, res, totalCost);
cout << "Minimum Spanning Trees: " << endl;
for (size_t i = 0; i < res.size(); ++i)
{
cout << "edge: " << res[i].u << " " << res[i].v << " " << res[i].cost << endl;
}
cout << endl;
cout << "num of edge: " << res.size() << endl;
cout << "total cost: " << totalCost << endl;
return 0;
}
Original Undirected Graphs示意图:
运行结果:
kruskal算法的时间复杂度主要来源于对边的排序,因此其时间复杂度为O(ElogE),其中E是图中边的个数。该算法适合顶点数较多,边数较少的情况,和prim算法正好相反。
总结:如果是稠密图(边多),选择prim算法,如果是稀疏图(边少),选择kruskal算法。