贪心算法——最小生成树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算法。

其主要思想是:

  1. 出发;将图中所有边按权值从小到大排序,按顺序取边;
  2. 从最小权值边开始,判断边的两个顶点是否在MST中,这里存在三种情况:
    (1)两个顶点都不在MST中,将两个顶点加入到MST中;
    (2)两个顶点其中一个在MST中,另一个不在MST中,同样,把这条边也加入到MST中;
    (3)这种情况也是最为复杂的一种,两个顶点都在MST中:经过了上面两种情况的连线MST后,可能会出现MST中的边不相连的情况(如图)
    贪心算法——最小生成树Kruskal算法_第1张图片
    这时,需要新加入MST的一条边又可以分为两种情况,若两个顶点分属不同的连通子图(连通子MST),则可以直接将邻接边加入到MST中;反之,若两个顶点都在同一子MST中,则可能造成回路,而生成树不能有回路,因此我们要舍弃;

如图所示
贪心算法——最小生成树Kruskal算法_第2张图片
根据第1、2步的要求,我们找到当前不在MST的边中,权值最小的:v2v4 = 3,此时我们就需要判断是否能将v2v4放入MST中。很显然,不能,这样就会产生回路
,因为v2和v4同属于同一个子MST,所以我们选择放弃这条边。
我们继续考虑下一步,下一条满足条件的边是v4v7 = 4。这时我们就可以将这条边放入MST中了,因为v4和v7属于不同的子MST,不会产生回路。

  1. 重复第二步,直至所有顶点都在MST中,即最小生成树构造完成。

贪心算法——最小生成树Kruskal算法_第3张图片

gif制作来源于VisuAlgo https://visualgo.net/zh/mst?slide=1

以上就是Kruskal算法的基本思路了,接下来就是具体实现。

  1. 我们这里不再采用二维数组邻接矩阵的形式储存图(一开始我仍然采用该数据结构,可后面发现在这里用处不大,若有其他函数,自行更改)。那我们采用什么数据结构呢?
    我们发现,在Kruskal中边起到了关键作用,因此我们先构造一个把保存边的结构体,保存边的两个顶点,边的权值。这样我们就可以采用一维数组的形式存储图了。
    我们还可以将数据结构进一步优化。
    我们发现,我们需要对边进行排序,然后每条边按顺序取,且每条边只用到了一次………………优先队列 (这里不再讲述优先队列的用法、定义)简而言之,优先队列就是可以自动排序的队列,在这里就满足了我们的需求。
    (有人可能会说用数组+排序也是可以的。当然可以,而且从时间复杂度来看,两者没什么区别。我使用优先队列图个方便。)
  2. Kruskal中的难点。在上面的分析中,第2步中前两种情况都很好判断,最后一种情况,判断两个顶点是否在同一子MST中,是Kruskal的难点也是关键部分。
    在这里我们用到了一个数据结构:并查集:当两个集合相互合并时,判断两个集合是否为同一个。可以很好的满足我们的需求:判断两个顶点是否输入同一个集合。
    并查集的写法在这里我们不在说明。详细写法参考:https://blog.csdn.net/niushuai666/article/details/6662911

源代码

#include
#include 
#include 

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
*/

运行结果

贪心算法——最小生成树Kruskal算法_第4张图片

你可能感兴趣的:(算法,#,贪心算法)