贪心算法——最小生成树Kruskal算法
·
最小生成树Kruskal算法
最小生成树(MST)是图论当中一个重要的算法,在实际生活中具有广泛的应用。有多种算法可以解决最小生成树问题,这里介绍Kruskal算法
问题描述
在一给定的无向图G = (V, E) 中,(u, v) 代表连接顶点 u 与顶点 v 的边,而 w(u, v) 代表此边的权重,若存在 T 为 E 的子集且为无循环图,使得的 w(T) 最小,则此 T 为 G 的最小生成树。
最小生成树其实是最小权重生成树的简称。
分析设计
Kruskal算法是解决最小生成树问题的经典算法之一。
解决最小生成树最常用的两种算法,一种是Kruskal算法,一种是Prim算法。
- Prim算法详见博客https://blog.csdn.net/weixin_42182525/article/details/100016792
下面介绍Kruskal算法。
其主要思想是:
- 从边出发;将图中所有边按权值从小到大排序,按顺序取边;
- 从最小权值边开始,判断边的两个顶点是否在MST中,这里存在三种情况:
(1)两个顶点都不在MST中,将两个顶点加入到MST中;
(2)两个顶点其中一个在MST中,另一个不在MST中,同样,把这条边也加入到MST中;
(3)这种情况也是最为复杂的一种,两个顶点都在MST中:经过了上面两种情况的连线MST后,可能会出现MST中的边不相连的情况(如图)

这时,需要新加入MST的一条边又可以分为两种情况,若两个顶点分属不同的连通子图(连通子MST),则可以直接将邻接边加入到MST中;反之,若两个顶点都在同一子MST中,则可能造成回路,而生成树不能有回路,因此我们要舍弃;
例
如图所示

根据第1、2步的要求,我们找到当前不在MST的边中,权值最小的:v2v4 = 3,此时我们就需要判断是否能将v2v4放入MST中。很显然,不能,这样就会产生回路
,因为v2和v4同属于同一个子MST,所以我们选择放弃这条边。
我们继续考虑下一步,下一条满足条件的边是v4v7 = 4。这时我们就可以将这条边放入MST中了,因为v4和v7属于不同的子MST,不会产生回路。
- 重复第二步,直至所有顶点都在MST中,即最小生成树构造完成。

gif制作来源于VisuAlgo https://visualgo.net/zh/mst?slide=1
以上就是Kruskal算法的基本思路了,接下来就是具体实现。
- 我们这里不再采用二维数组邻接矩阵的形式储存图(一开始我仍然采用该数据结构,可后面发现在这里用处不大,若有其他函数,自行更改)。那我们采用什么数据结构呢?
我们发现,在Kruskal中边起到了关键作用,因此我们先构造一个把保存边的结构体,保存边的两个顶点,边的权值。这样我们就可以采用一维数组的形式存储图了。
我们还可以将数据结构进一步优化。
我们发现,我们需要对边进行排序,然后每条边按顺序取,且每条边只用到了一次………………优先队列 (这里不再讲述优先队列的用法、定义)简而言之,优先队列就是可以自动排序的队列,在这里就满足了我们的需求。
(有人可能会说用数组+排序也是可以的。当然可以,而且从时间复杂度来看,两者没什么区别。我使用优先队列图个方便。) - Kruskal中的难点。在上面的分析中,第2步中前两种情况都很好判断,最后一种情况,判断两个顶点是否在同一子MST中,是Kruskal的难点也是关键部分。
在这里我们用到了一个数据结构:并查集:当两个集合相互合并时,判断两个集合是否为同一个。可以很好的满足我们的需求:判断两个顶点是否输入同一个集合。
并查集的写法在这里我们不在说明。详细写法参考:https://blog.csdn.net/niushuai666/article/details/6662911
源代码
#include<iostream>
#include <vector>
#include <queue>
using namespace std;
//边结构
typedef struct Edge {
int vexa;
int vexb;
int value;
}Edge;
vector<Edge> Tree; //最小生成树
//运算符重载,优先队列升序排列
typedef struct temp {
bool operator()(Edge a, Edge b) {
return a.value > b.value;
}
};
priority_queue < Edge, vector<Edge>, temp> q; //优先队列排序边
//并查集存放上一级结点
vector<int> pre;
//并查集查找根节点
int unionsearch(int root){
int son, tmp;
son = root;
while (root != pre[root]) //寻找根结点
root = pre[root];
while (son != root){ //路径压缩
tmp = pre[son];
pre[son] = root;
son = tmp;
}
return root;
}
//判断两个点是否在同一并查集
bool join(int root1, int root2){ //判断是否连通,不连通就合并
int x, y;
x = unionsearch(root1);
y = unionsearch(root2);
if (x == y) //如果连通返回true
return true;
else { //如果不连通,就把它们所在的连通分支合并
pre[x] = y;
return false;
}
}
//算法
int Kruskal() {
int sum = 0; //总权值
while (!q.empty()) {
Edge edge = q.top();
int a = edge.vexa;
int b = edge.vexb;
if (!join(a, b)) { //a,b顶点 不在同一并查集(不在同一棵树),则合并
Tree.push_back(edge); //加入到最小生成树中
sum += edge.value;
}
q.pop();
}
return sum;
}
int main() {
int vexNum, edgeNum;
cout << "输入边数:";
cin >> vexNum >> edgeNum;
cout << "输入邻接边和权值a b w:" << endl;
int a, b, w;
for (int i = 0; i < edgeNum; i++) {
cin >> a >> b >> w;
Edge edge; //将边信息保存并放入优先队列中
edge.vexa = a;
edge.vexb = b;
edge.value = w;
q.push(edge); //入队
}
//初始化并查集
pre.resize(vexNum + 1);
for (int i = 1; i <= vexNum; i++)
pre[i] = i;
int sum = Kruskal(); //Kruskal算法
cout << endl << "最小生成树组成:" << endl;
for (unsigned i = 0; i < Tree.size(); i++) {
cout << Tree[i].vexa << " -> " << Tree[i].vexb << " = " << Tree[i].value << endl;
}
cout << "总权值为:" << sum << endl;
system("pause");
return 0;
}
/*
1 2 2
1 3 4
1 4 1
2 4 3
2 5 10
3 4 2
3 6 5
4 5 7
4 6 8
4 7 4
5 7 6
6 7 1
*/
运行结果

更多推荐
所有评论(0)