C语言 最小生成树 (Kruskal算法和Prim算法)

前言

在一个加权连通图中,最小生成树(Minimum Spanning Tree,简称MST)就是连接所有节点的一棵树,并且使得树上边的总权值最小。这个树又被称为图的“最小权重生成树”。

最小生成树问题是一个重要的组合优化问题,在很多现实应用中都有广泛的应用,比如网络规划、电力工程设计、交通运输等领域。

最小生成树问题可以通过解决环路问题来得到解决。如果移除加权连通图中所有环路,那么剩下的就是一棵最小生成树。由于最小生成树只包含 n-1 条边,因此也是可行运输树的一种特殊形式。

求解最小生成树问题的经典算法包括普里姆(Prim)算法克鲁斯卡尔(Kruskal)算法。


视频讲解-----> 

最小生成树 Prim算法和Kruskal算法


 

普里姆算法

Prim算法是基于贪心策略的算法,其基本思想是以一个点为起点开始,每次选择一条与当前生成树相邻的最短边,将其加入生成树中,直到所有点都被加入生成树为止。

基本流程:

  1. 定义辅助数组dist(各顶点离当前生成树距离)、visited(标记顶点是否加入生成树)、parent(生成树中每个节点的父节点)。
  2. 将各顶点到生成树的距离设为正无穷大,表示暂时无法到达。置起点u的dist[u]为0。
  3. 找到离当前生成树最近的顶点t,将其加入生成树并标记。
  4. 更新顶点t未被标记的邻接点离当前生成树的距离,并更新其父节点。
  5. 重复步骤3和4,直到所有点都被加入生成树为止。
  6. 打印最小生成树以及权值和。

需要注意的是,如果原图不连通,则最终生成的树只是原图的一个连通分量的最小生成树,需要对每个连通分量分别进行求解。

无向网G以邻接矩阵形式储存,从顶点u出发构造最小生成树。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MVNum 100//最大顶点数
#define MaxInt 66666//表示极大值
typedef struct {
	char vexs[MVNum];//顶点表(顶点为字符型)
	int arcs[MVNum][MVNum];//邻接矩阵(权值为整型)
	int vexnum, arcnum;//图的当前点数和边数
}AMGraph;

//定位
int LocateVex(AMGraph* G, char v) {
	int i;
	for (i = 0; i < G->vexnum; i++) {
		if (G->vexs[i] == v) {
			return i;
		}
	}
	return -1;
}
//创建无向网G
AMGraph* CreateUDN() {
	int i, j, k, w;
	char v1, v2;
	AMGraph* G = malloc(sizeof(AMGraph));
	printf("输入总顶点数,边数\n");
	scanf("%d%d", &G->vexnum, &G->arcnum);
	getchar();//吸收换行符
	printf("依次输入点的信息\n");
	for (i = 0; i < G->vexnum; i++) {
		scanf("%c", &G->vexs[i]);
	}
	getchar();//吸收换行符
	for (i = 0; i < G->vexnum; i++)
		for (j = 0; j < G->vexnum; j++) {
			if (i == j) {
				G->arcs[i][j] = 0;
			}
			else {
				G->arcs[i][j] = MaxInt;
			}
		}
	for (k = 0; k < G->arcnum; k++) {
		printf("输入一条边依附的顶点及权值\n");
		scanf("%c%c", &v1, &v2);
		scanf("%d", &w);
		getchar();//吸收换行符
		i = LocateVex(G, v1), j = LocateVex(G, v2);//确定v1、v2在顶点数组的下标
		G->arcs[i][j] = w;//边<v1,v2>权值置为w
		G->arcs[j][i] = w;//无向网对称边<v2,v2>权值也置为w
	}
	return G;
}

//普里姆算法
void Prim(AMGraph* G, int u) {
	//u为起点
	int dist[MVNum];//储存各顶点离集合U的距离
	bool visited[MVNum];//标记顶点是否加入生成树
	int parent[MVNum];//生成树中每个节点对应的父节点
	int i, j, k, t, min_dis;
	//初始化
	for (i = 0; i < G->vexnum; i++) {
		dist[i] = MaxInt;
		visited[i] = false;
	}
	dist[u] = 0;
	parent[u] = -1;
	for (i = 1; i < G->vexnum; i++) {
		t = -1;
		min_dis = MaxInt;
		//找到离当前生成树最近的顶点t。
		for (j = 0; j < G->vexnum; j++) {
			if (!visited[j] && dist[j] < min_dis) {
				t = j;
				min_dis = dist[j];
			}
		}
		if (t == -1)	break;//生成树无法延伸
		visited[t] = true;//标记顶点t
		//更新顶点t未被标记的邻接点离当前生成树的距离,并更新其父节点。
		for (k = 0; k < G->vexnum; k++) {
			if (!visited[k] && G->arcs[t][k] < dist[k]) {
				dist[k] = G->arcs[t][k];
				parent[k] = t;
			}
		}
	}
	//打印最小生成树以及权值和
    printf("最小生成树:\n");
	int count = 0;
	for (i = 0; i < G->vexnum; i++) {
		if (parent[i] != -1) {
			printf("<%c,%c>  ", G->vexs[parent[i]], G->vexs[i]);
		}
		count += dist[i];
	}
	printf("\n权值和为:%d\n", count);
}

int main() {
	AMGraph* G = CreateUDN();
	Prim(G, 0);
	return 0;
}

运行代码,构造下图无向网的最小生成树: 

运行结果: 


Kruskal算法

克鲁斯卡尔算法是一种基于并查集的算法。其主要思想是将所有边按照权重从小到大进行排序,依次加入边,直到连接所有点,但要确保新加入的边不会形成环。

基本流程:

  1. 定义结构体数组Edges(储存边的信息)和并查集数组Vexset。
  2. 调用快排函数将数组Edges中的所有边按照权重从小到大进行排序。
  3. 初始化并查集Vexset,将每一个顶点都看作一个单独的集合。由于每个集合只包含一个元素,因此该元素即为该集合代表。
  4. 依次选择权重最小的边,并判断该边连接的两个顶点是否属于同一个集合(可以通过并查集来实现)。如果不在同一个集合中,则合并这两个集合,更新权重和,并输出该边。
  5. 输出权重和。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MVNum 100//最大顶点数
#define MaxInt 66666//表示极大值
typedef struct {
	char vexs[MVNum];//顶点表(顶点为字符型)
	int arcs[MVNum][MVNum];//邻接矩阵(权值为整型)
	int vexnum, arcnum;//图的当前点数和边数
}AMGraph;

//定位
int LocateVex(AMGraph* G, char v) {
	int i;
	for (i = 0; i < G->vexnum; i++) {
		if (G->vexs[i] == v) {
			return i;
		}
	}
	return -1;
}
//创建无向网G
AMGraph* CreateUDN() {
	int i, j, k, w;
	char v1, v2;
	AMGraph* G = malloc(sizeof(AMGraph));
	printf("输入总顶点数,边数\n");
	scanf("%d%d", &G->vexnum, &G->arcnum);
	getchar();//吸收换行符
	printf("依次输入点的信息\n");
	for (i = 0; i < G->vexnum; i++) {
		scanf("%c", &G->vexs[i]);
	}
	getchar();//吸收换行符
	for (i = 0; i < G->vexnum; i++)
		for (j = 0; j < G->vexnum; j++) {
			if (i == j) {
				G->arcs[i][j] = 0;
			}
			else {
				G->arcs[i][j] = MaxInt;
			}
		}
	for (k = 0; k < G->arcnum; k++) {
		printf("输入一条边依附的顶点及权值\n");
		scanf("%c%c", &v1, &v2);
		scanf("%d", &w);
		getchar();//吸收换行符
		i = LocateVex(G, v1), j = LocateVex(G, v2);//确定v1、v2在顶点数组的下标
		G->arcs[i][j] = w;//边<v1,v2>权值置为w
		G->arcs[j][i] = w;//无向网对称边<v2,v2>权值也置为w
	}
	return G;
}

struct Edge {
	char Head;//边的始点
	char Tail;//边的终点
	int weight;//边的权重
};

//快排函数的比较函数
int cmp(const void* a, const void* b) {
	return ((struct Edge*)a)->weight - ((struct Edge*)b)->weight;
}

//克鲁斯卡尔算法
void Kruskal(AMGraph* G) {
	struct Edge Edges[MVNum];//储存边的结构体数组
	int Vexset[MVNum];//并查集
	int i, j, k, v1, v2, vs1, vs2;
	int count = 0;//储存权重和
	//将图中所有边存入数组Edges
	for (i = 0, k = 0; i < G->vexnum; i++) {
		for (j = i + 1; j < G->vexnum; j++) {
			if (G->arcs[i][j] != 0 && G->arcs[i][j] != MaxInt) {
				Edges[k++] = (struct Edge){ G->vexs[i],G->vexs[j],G->arcs[i][j] };
			}
		}
	}
	//调用快排函数,按权重从小到大排序
	qsort(Edges, G->arcnum, sizeof(struct Edge), cmp);
	//初始化并查集
	for (i = 0; i < G->vexnum; i++) {
		Vexset[i] = i;
	}
	//遍历数组Edges中的边
	for (i = 0; i < G->arcnum; i++) {
		v1 = LocateVex(G, Edges[i].Head);//该边的始点序号
		v2 = LocateVex(G, Edges[i].Tail);//该边的终点序号
		vs1 = Vexset[v1];//vs1为顶点v1所属集合编号
		vs2 = Vexset[v2];//vs2为顶点v2所属集合编号
		//编号不相等时,说明顶点v1和v2不属于同一个集合
		if (vs1 != vs2) {
			printf("<%c,%c>  ", G->vexs[v1], G->vexs[v2]);//输出此边
			count += G->arcs[v1][v2];//更新权重和
			//合并这两个集合,即统一编号
			for (j = 0; j < G->vexnum; j++) {
				//集合编号为vs2的都改为vs1
				if (Vexset[j] == vs2) {
					Vexset[j] = vs1;
				}
			}
		}
	}
	printf("\n权重和为%d\n", count);
}

int main() {
	AMGraph* G = CreateUDN();
	printf("最小生成树:\n");
	Kruskal(G);
	return 0;
}

运行程序,求下图最小生成树: 

 运行结果:


总结

以上算法的实现,普里姆算法的时间复杂度为O(n^2),与网中边上无关,因此适用于求稠密网的最小生成树;克鲁斯卡尔算法时间复杂度为O(eloge),与网中边数有关,与普里姆算法相比,更适合求稀疏网的最小生成树。

 

  • 4
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
Prim算法Kruskal算法都是用于求解最小生成树的经典算法Prim算法的基本思想是从一个点开始,每次选择一个与当前生成树距离最近的点加入生成树中,直到所有点都被加入生成树为止。具体实现时,可以使用一个优先队列来维护当前生成树与未加入生成树的点之间的距离,每次选择距离最小的点加入生成树中。 Kruskal算法的基本思想是从边开始,每次选择一条权值最小且不会形成环的边加入生成树中,直到生成树中包含所有点为止。具体实现时,可以使用并查集来判断是否形成环。 下面是Prim算法Kruskal算法C语言代码实现: Prim算法: ```c #include <stdio.h> #include <stdlib.h> #include <limits.h> #define MAX_VERTICES 1000 int graph[MAX_VERTICES][MAX_VERTICES]; int visited[MAX_VERTICES]; int dist[MAX_VERTICES]; int prim(int n) { int i, j, u, min_dist, min_index, sum = 0; for (i = 0; i < n; i++) { visited[i] = 0; dist[i] = INT_MAX; } dist[0] = 0; for (i = 0; i < n; i++) { min_dist = INT_MAX; for (j = 0; j < n; j++) { if (!visited[j] && dist[j] < min_dist) { min_dist = dist[j]; min_index = j; } } u = min_index; visited[u] = 1; sum += dist[u]; for (j = 0; j < n; j++) { if (!visited[j] && graph[u][j] < dist[j]) { dist[j] = graph[u][j]; } } } return sum; } int main() { int n, m, i, j, u, v, w; scanf("%d%d", &n, &m); for (i = 0; i < n; i++) { for (j = 0; j < n; j++) { graph[i][j] = INT_MAX; } } for (i = 0; i < m; i++) { scanf("%d%d%d", &u, &v, &w); graph[u][v] = graph[v][u] = w; } printf("%d\n", prim(n)); return 0; } ``` Kruskal算法: ```c #include <stdio.h> #include <stdlib.h> #include <limits.h> #define MAX_VERTICES 1000 #define MAX_EDGES 1000000 struct edge { int u, v, w; }; int parent[MAX_VERTICES]; struct edge edges[MAX_EDGES]; int cmp(const void *a, const void *b) { return ((struct edge *)a)->w - ((struct edge *)b)->w; } int find(int x) { if (parent[x] == x) { return x; } return parent[x] = find(parent[x]); } void union_set(int x, int y) { parent[find(x)] = find(y); } int kruskal(int n, int m) { int i, sum = 0; for (i = 0; i < n; i++) { parent[i] = i; } qsort(edges, m, sizeof(struct edge), cmp); for (i = 0; i < m; i++) { if (find(edges[i].u) != find(edges[i].v)) { union_set(edges[i].u, edges[i].v); sum += edges[i].w; } } return sum; } int main() { int n, m, i; scanf("%d%d", &n, &m); for (i = 0; i < m; i++) { scanf("%d%d%d", &edges[i].u, &edges[i].v, &edges[i].w); } printf("%d\n", kruskal(n, m)); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Indifferent-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值