最小生成树

1. 生成树与最小生成树

生成树:无向连通图 G 的一个子图如果是一棵包含 G 的所有顶点的树,则该子图称为 G 的生成树。生成树是连通图的极小连通子图。这里所谓极小是指:若在树中任意增加一条边,则将出现一个回路;若去掉一条边,将会使之变成非连通图。 

最小生成树:对无向连通图的生成树,各边的权值总和称为生成树的权,权最小的生成树称为最小生成树。

构造最小生成树的准则有三条:

  • 必须只使用该网络中的边来构造最小生成树;
  • 必须使用且仅使用 n-1 条边来连接网络中的 n 个顶点;
  • 不能使用产生回路的边。 

构造最小生成树的算法主要有:克鲁斯卡尔(Kruskal)算法、 Boruvka 算法和普里姆(Prim)算法,都得遵守以上准则。 

2. Kruskal 算法思想 

克鲁斯卡尔算法的基本思想是以边为主导地位,始终都是选择当前可用的最小权值的边。具体为:

  • 设一个有 n 个顶点的连通网络为 G(V, E)最初先构造一个只有 n 个顶点,没有边的非连通图 T = { V, Ø },图中每个顶点自成一个连通分量
  • 当在 E 中选择一条具有最小权值的边时,若该边的两个顶点落在不同的连通分量上,则将此边加入到 T ;否则,即这条边的两个顶点落在同一个连通分量上,则将此边舍去(此后永不选用这条边),重新选择一条权值最小的边。
  • 如此重复下去,直到所有顶点在同一个连通分量上为止。 

如下图(a)所示的无向网,其邻接矩阵如图(b)所示。利用克鲁斯卡尔算法构造最小生成树的过程如图(c)所示,首先构造的是只有 7 个顶点,没有边的非连通图。剩下的过程为(图(c)中的每条边旁边的序号跟下面的序号是一致的):

  • 在边的集合 E 中选择权值最小的边,即(1, 6),权值为 10
  • 在集合 E 剩下的边中选择权值最小的边,即(3, 4),权值为 12
  • 在集合 E 剩下的边中选择权值最小的边,即(2, 7),权值为 14
  • 在集合 E 剩下的边中选择权值最小的边,即(2, 3),权值为 16
  • 在集合 E 剩下的边中选择权值最小的边,即(7, 4),权值为 18,但这条边的两个顶点位于同一个连通分量上,所以要舍去;继续选择一条权值最小的边,即(4, 5),权值为 22
  • 在集合 E 剩下的边中选择权值最小的边,即(7, 5),权值为 24,但这条边的两个顶点位于同一个连通分量上,所以要舍去;继续选择一条权值最小的边,即(6, 5),权值为 25。至此,最小生成树构造完毕,最终构造的最小生成树如图(d)所示,生成树的权为 99。 

克鲁斯卡尔算法的伪代码为: 

T = (V, φ);
while ( T 中所含边数 < n-1 )
{
    从 E 中选取当前权值最小的边(u, v);
    从 E 中删除边(u, v);
    if(边(u, v)的两个顶点落在两个不同的连通分量上) {
        将边(u, v)并入 T 中;
    }
}

Kruskal 算法在每选择一条边加入到生成树集合 T 时,有两个关键步骤:

  • E 中选择当前权值最小的边(u, v),实现时可以用最小堆来存放 E 中所有的边;或者将所有边的信息(边的两个顶点、权值)存放到一个数组 edges 中,并将 edges 数组按边的权值从小到大进行排序,然后依先后顺序选用每条边。 
  • 选择权值最小的边后,要判断两个顶点是否属于同一个连通分量,如果是,则要舍去;如果不是,则选用,并将这两个顶点分别所在的连通分量合并成一个连通分量。在实现时可以使用并查集来判断两个顶点是否属于同一个连通分量、以及将两个连通分量合并成一个连通分量。 

Kruskal 算法求最小生成树实现代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

#define MAXL 9 // 定义边个数
#define MAXM 7 // 定义顶点个数

using namespace std;

int Parent[MAXM];
int Rank[MAXM];

int Find(int x) {
	int t = x;
	while (Parent[t] != -1) {
		t = Parent[t];
	}
	return t;
}

int Join(int x, int y) {
	int p_x = Find(x);
	int p_y = Find(y);

	if (p_x == p_y) {
		return 0;
	}
	else if (Rank[p_x] > Rank[p_y]) {
		Parent[p_y] = p_x;
	}
	else if (Rank[p_y] > Rank[p_x]) {
		Parent[p_x] = p_y;
	}
	else {
		Parent[p_y] = p_x;
		Rank[p_x]++;
	}
	return 1;
}

struct Edge {
	int u, v, w;

	Edge(int _u, int _v, int _w): u(_u), v(_v), w(_w) {}
};

int main() {

	int u, v, w;
	int total = 0;

	memset(Parent, -1, sizeof(Parent));
	memset(Rank, 0, sizeof(Rank));

	vector<Edge> vec;


	for (int i = 0; i < MAXL; i++) {
		cin >> u >> v >> w;
		Edge edge(u, v, w);
		vec.push_back(edge);
	}

	sort(vec.begin(), vec.end(), [](Edge e1, Edge e2)->bool { // 按边的权值进行排序
		return e1.w < e2.w;
	});

	int cnt = 0; // 统计已经加入的边个数

	for (int i = 0; i < vec.size(); i++) {
		int v1 = vec[i].u;
		int v2 = vec[i].v;
		
		if (Join(v1, v2) != 0) { // 利用并查集判断是否存在环
			cout << vec[i].w << endl;
			if (cnt < MAXM - 1) { // n个顶点的最小生成树有n - 1条边
				total += vec[i].w; // 按照从大到小的顺序加入n - 1条边
				cnt++;
			}
			else {
				break;
			}
		}
	}

	cout << total;

	return 0;
}

3. Boruvka 算法 

Boruvka 算法是最古老的一个 MST 算法,其思想类似于 Kruskal 算法思想。 Boruvka 算法可以分为两步:

  • 对图中各顶点,将与其关联、具有最小权值的边选入 MST,得到的是由 MST 子树构成的森林;
  • 在图中陆续选择可以连接两颗不同子树、且具有最小权值的边,将子树合并,最终构造 MST 

例如,对上图(a)所示的无向连通图,与顶点 1 关联的、权值最小的边为边(1, 6),将其选入MST,与顶点 6 关联的、权值最小的边也为边(1, 6),这条边将顶点 1 6 连接成 MST 中的第一棵子树;按照类似的方法,得到另外两棵子树:顶点 2、顶点 7 组成的第二棵子树顶点 345 组成第三棵子树,如下图所示。这是第一步。 

第二步,选择边(2, 3)将第二、三棵子树合并,以及选择边(5, 6)再将第一棵子树合并进来,至此 MST 构造完毕,如下图(b)所示。

4. Prim 算法思想

普里姆算法的基本思想是以顶点为主导地位:从起始顶点出发,通过选择当前可用的最小权值边依次把其他顶点加入到生成树当中来。

设连通无向网为 G(V, E),在普里姆算法中,将顶点集合 V 分成两个子集合 T T'

  • T当前生成树顶点集合;
  • T'不属于当前生成树的顶点集合。很显然有: TT'= V

普里姆算法的具体过程为  

  • 从连通无向网 G 中选择一个起始顶点 u0, 首先将它加入到集合 T 中; 然后选择与 u0 关联的、具有最小权值的边(u0, v),将顶点 v 加入到顶点集合 T 中。
  • 以后每一步从一个顶点(设为 u)在 T 中,而另一个顶点(设为 v)在 T'中的各条边中选择权值最小的边(u, v),把顶点 v 加入到集合 T 中。如此继续下去,直到网络中的所有顶点都加入到生成树顶点集合 T 中为止。

算法思想:

在普里姆算法运算过程当中,需要知道以下两类信息:

  • 集合T'内各顶点距离 T 内各顶点权值最小的边的权值;
  • 集合 T'内各顶点距离 T 内哪个顶点最近(即边的权值最小)。为了存储和表示这两类信息,必须定义 2 个辅助数组: 
    • lowcost[ ]: 存放顶点集合 T'内各顶点到顶点集合 T 内各顶点权值最小的边的权值。 
    • nearvex[ ]: 记录顶点集合 T'内各顶点距离顶点集合 T 内哪个顶点最近; 当 nearvex[i]-1时,表示顶点 i 属于集合 T。

以上图所示的无向网为例,如果选择的起始顶点为顶点 1,则这 2 个辅助数组的初始状态为: 

这是因为在生成树顶点集合 T 内最初只有一个顶点,即顶点 1,因此在 nearvex 数组中,只有表示顶点 1 的数组元素 nearvex[1] = -1,其他元素值都是 1,表示集合 T'内各顶点距离集合 T内最近的顶点是顶点 1; 

prim 算法里要重复做以下工作:

  • lowcost[ ]数组中选择 nearvex[i] != -1 lowcost[i]最小的顶点 i,用 v 标记它。则选中的权值最小的边为(nearvex[v], v),相应的权值为 lowcost[v]。例如在上图中,第一次选中的 v=6,则边(1, 6)是选中的权值最小的边,相应的权值为 lowcost[6] = 10
  • nearvex[v]改为-1,表示它已经加入到生成树顶点的集合 T,将边(nearvex[v], v,lowcost[v])输出来。
  • 修改 lowcost[ ]:注意,原来顶点 v 不属于生成树顶点集合,现在 v 加入进来了,则顶点集合 T'内的各顶点(设为顶点 i)到顶点集合 T 内的权值最小的边的权值要修改成:lowcost[i] = min{ lowcost[i], Edge[v][i] },即把顶点 i 到新加入集合 T 的顶点 v 的距离(Edge[v][i])与原来它到集合 T 中顶点的最短距离(lowcost[i])做比较,取距离近的,作为顶点 i 到 T 内顶点的最短距离。
  • 修改 nearvex[ ]:如果 T'内顶点 i 到顶点 v 的距离比原来它到顶点集合 T 中顶点的最短距离还要近,则修改 nearvex[i]nearvex[i] = v;表示顶点 i 当前到 T 内顶点 v 的距离最近。

下图演示了在求上图所示的无向网的最小生成树过程中 lowcost 数组和 nearvex 数组各元素值的变化。 

算法实现:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

#define INF 10000
#define MAXL 21


using namespace std;

int n, m;

// 设已进入到MST集合的点在T中,未进入的点在T`中

int Edge[MAXL][MAXL];
int lowcost[MAXL];  // 表示T中的点与T`中的点形成的最小权值的边的权值
int nearvex[MAXL];  // 表示T中的点距离T`中哪个点拥有最小权值

void Prim(int u0) {
	int total = 0; // MST的权值统计

	for (int i = 1; i <= n; i++) { // 对lowcast、nearvex数组进行初始化
		lowcost[i] = Edge[u0][i];
		nearvex[i] = u0;
	}

	nearvex[u0] = -1; // 表示起始点u0已经在T集合中

	for (int i = 1; i < n; i++) {

		int Min = INF;
		int v = -1;

		for (int j = 1; j <= n; j++) {
			if (nearvex[j] != -1 && lowcost[j] < Min) {
				v = j;
				Min = lowcost[j];
			}
		}

		if (v != -1) {
			cout << nearvex[v] << ' ' << v << ' ' << lowcost[v] << endl;

			nearvex[v] = -1; // 把v加入T集合,-1表示v在T内
			total += lowcost[v];

			// 以v为起点再次Prim
			for (int s = 1; s <= n; s++) {
				if (nearvex[s] != -1 && Edge[v][s] < lowcost[s]) {
					lowcost[s] = Edge[v][s];
					nearvex[s] = v;
				}
			}
		}
	}
	cout << total;
}

int main() {
	int u, v, w;

	memset(Edge, 0, sizeof(Edge));

	cin >> n >> m;

	for (int i = 1; i <= m; i++) { // 输入存在的边
		cin >> u >> v >> w;
		Edge[u][v] = Edge[v][u] = w;
	}

	for (int i = 1; i <= n; i++) { // 对于不存在的边(即权值为0)将u-v的w设置为无穷
		for (int j = 1; j <= n; j++) {
			if (i == j) {
				Edge[i][j] == 0;
			}
			else if (Edge[i][j] == 0) {
				Edge[i][j] = INF;
			}
		}
	}

	Prim(1); // 传入的参数表示,以哪一个点作为起始点开始Prim

	return 0;
}

 

posted @   小熊酱  阅读(35)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示