欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

最小生成树算法:普利姆、克鲁斯卡尔(附详细代码)

程序员文章站 2022-07-13 08:38:42
...

【注:】本文代码在c++环境下运行

普利姆算法和克鲁斯卡尔算法,都可以用于最小生成树的寻找。

初学两种算法,很容易混淆,经常会把普利姆的算法过程记到了克鲁斯卡尔的头上。这是因为这两个算法都是以人名命名,难以记忆。所以,我建议,根据两算法的特点,把克鲁斯卡尔算法记作 “加边算法” 。把普利姆算法记作 “加点算法”

我们今天的算法分析,就以下面这个无向带权图为例子:
最小生成树算法:普利姆、克鲁斯卡尔(附详细代码)
其邻接矩阵我们可以画出,如下图:
最小生成树算法:普利姆、克鲁斯卡尔(附详细代码)

加边算法克鲁斯卡尔

最小生成树算法:普利姆、克鲁斯卡尔(附详细代码)

就是每次选择权值最小的边加入到最小生成树集合中。当然,要满足一个前提,这条边加入进来之后,不能产生回路。

要解决的核心问题就是:1.排序;2.判断一条边的加入,会不会构成环。

核心代码:(详细代码见帖子最后)

//克鲁斯卡尔算法
void kruskal(EData **edges, PGraph G) {
	int k = 0;			//对应结果集的指针 
	EData res[MAX];		//存最小生成树结果的边
	sort(edges, G);		//对边的权值排序
	//遍历每一条边
	for(int i = 0;i < G->edgnum;i++){		
		//把每一条边的两个结点编号拿出来
		int p_start = get_index((*edges + i)->start);
		int p_end = get_index((*edges + i)->end);
		//p_start和 p_end 如果不属于同一个集合才能加入。否则加入他们就会产生回路。 
		if(G->vexs[p_start] != G->vexs[p_end]){
			res[k++] = *(*edges + i);
			//要把所有连通分量编号为 p_end的节点给置为p_start。这一步是至关重要的一步!! 
			for(int j = 0;j < G->vexnum;j++){
				if(G->vexs[j] == G->vexs[p_end]){
					G->vexs[j] = G->vexs[p_start];
				}
			}
		}
	}
	cout << "克鲁斯卡尔算法生成的最小生成树的各边为:" << endl;
	for(int i = 0;i < k;i++){
		cout << res[i].start << ", " << res[i].end << "权值为:" << res[i].weight << endl;
	}
}

加点算法普利姆:

最小生成树算法:普利姆、克鲁斯卡尔(附详细代码)
设定一个U集合,起始阶段把源点加入U集合内部,

每次把离 U集合 最近的那个结点加入U集合内,然后更新U集合到U集合外其他各点的距离,更新的过程我们称为 松弛

再说大白话一点,每次把权值最小的外部结点加入U集合内。等所有结点都加入进U集合了,算法终止,最小生成树也成了。

核心代码:(完整代码见帖子最后)

//普利姆算法
int* prim(PGraph G, int *sum) {
	int* res = (int*)malloc(sizeof(EData) * G->vexnum);int k = 1;*sum = 0;	//res数组装每次选择加入最小生成树的结点序号 
	*res = 0;		//第一个节点作为初始节点 
	for(int i = 0;i < G->vexnum;i++){	//初始化dis数组 和 src数组 
		G->dis[i] = G->matrix[0][i];
		G->src[i] = 0;						
	}
	for(int i = 0;i < G->vexnum;i++){
		int min = MAX_INT, min_index = 0;
		//每次找出dis数组中的最小值,作为下一个节点
		for(int j = 0;j < G->vexnum;j++){
			if(G->dis[j] != 0 && G->dis[j] < min){
				min = G->dis[j];
				min_index = j;
			}
		} 
		//结果更新
		*sum = *sum + G->dis[min_index];
		G->dis[min_index] = 0;			//min_index结点就进入U集合了 
		*(res + k) = min_index;
		k++;
		//dis数组更新。根据这个最小值节点,松弛附近的权值
		for(int j = 0;j < G->vexnum;j++){
			//考虑到负权值的情况 
			if(G->matrix[min_index][j] < G->dis[j] && G->dis[j] != 0){
				G->dis[j] = G->matrix[min_index][j];
				G->src[j] = min_index;		//要更改路径 
			}
		} 
	}
	return res;
}

下面是完整可运行代码,包含所有数据结构及方法:

克鲁斯卡尔的:

#include<iostream>
#include<cstdlib>
using namespace std;

#define MAX_INT 999	 //边的最大权值,视为正无穷 
#define MAX 100		//数组最大长度 
/*数据结构*
// 邻接矩阵
*/
typedef struct _graph
{
    char vexs[MAX];       // 顶点集合
    int vexnum;           // 顶点数
    int edgnum;           // 边数
    int matrix[MAX][MAX]; // 邻接矩阵
}Graph, *PGraph;

// 边的结构体
typedef struct _EdgeData
{
    char start; // 边的起点
    char end;   // 边的终点
    int weight; // 边的权重
}EData;

/*
**/

//初始化邻接矩阵 
void init(PGraph G)
{
	//所有顶点 
	G->vexs[0] = 'A';G->vexs[1] = 'B';G->vexs[2] = 'C';G->vexs[3] = 'D';G->vexs[4] = 'E';
	G->edgnum = 7;
	G->vexnum = 5;
	//填充邻接矩阵
	G->matrix[0][1] = 9;G->matrix[0][2] = 8;G->matrix[0][3] = MAX_INT;G->matrix[0][4] = MAX_INT;
	G->matrix[1][2] = 2;G->matrix[1][3] = MAX_INT;G->matrix[1][4] = 6;
	G->matrix[2][3] = 4;G->matrix[2][4] = 5;
	G->matrix[3][4] = 3;
	//根据对称性质
	for(int i = 1;i < G->vexnum;i++){
		for(int j = 0;j < i;j++){
			G->matrix[i][j] = G->matrix[j][i];
		}	
	} 
}

void show(PGraph G)
{
	for(int i = 0;i < G->vexnum;i++){
		for(int j = 0;j < G->vexnum;j++){
			if(G->matrix[i][j] == MAX_INT)
				cout << "#" << " ";				//无穷大用#表示 
			else
				cout << G->matrix[i][j] << " ";
		}	
		cout << endl;
	} 
}

//得到该图的边集合。因为函数内部要改指针edges的值。所以要用二级指针 
void getEdges(EData **edges, PGraph G)
{
	*edges = (EData *)malloc(G->edgnum * sizeof(EData));
	int incre = 0;
	for(int i = 1;i < G->vexnum;i++){
		for(int j = 0;j < i;j++){
			if(G->matrix[i][j] != MAX_INT){
				//找到起点和终点 
				char start = G->vexs[i];
				char end = G->vexs[j];
				(*edges + incre)->start = start;
				(*edges + incre)->end = end;
				(*edges + incre)->weight = G->matrix[i][j];
				//地址往后增 
				incre++;
			}
		}	
	} 
} 

//打印所有的边 
void showEdges(EData *edges, PGraph G){
	for(int i = 0;i < G->edgnum;i++){
		cout << (edges + i)->start << ", " << (edges + i)->end << ", " << (edges + i)->weight << endl;
	}
}

//对Edge数组进行排序。采用简单选择排序算法 
void sort(EData **edge, PGraph G){
	for(int i = 0;i < G->edgnum;i++){
		int min = (*edge + i)->weight;
		int min_index = i;
		for(int j = i + 1;j < G->edgnum;j++){
			int val = (*edge + j)->weight;
			if(val < min){
				min = val;
				min_index = j;
			}
		}
		//和最小的节点交换 
		EData temp;
		temp.end = (*edge + i)->end;temp.start = (*edge + i)->start;temp.weight = (*edge + i)->weight;
		(*edge + i)->end = (*edge + min_index)->end;(*edge + i)->start = (*edge + min_index)->start;(*edge + i)->weight = (*edge + min_index)->weight;
		(*edge + min_index)->end = temp.end;(*edge + min_index)->start = temp.start;(*edge + min_index)->weight = temp.weight;
	}
}

//通过节点序号得到 其vexs的 下标 
int get_index(char vex){
	return vex - 65;
}

//克鲁斯卡尔算法
void kruskal(EData **edges, PGraph G) {
	int k = 0;			//对应结果集的指针 
	EData res[MAX];		//存最小生成树结果的边
	sort(edges, G);
	for(int i = 0;i < G->edgnum;i++){
		int p_start = get_index((*edges + i)->start);
		int p_end = get_index((*edges + i)->end);
		//p_start和 p_end 如果不属于同一个集合才能加入。否则加入他们就会产生回路。 
		if(G->vexs[p_start] != G->vexs[p_end]){
			res[k++] = *(*edges + i);
			//要把所有连通分量编号为 p_end的节点给置为p_start。这一步是至关重要的一步!! 
			for(int j = 0;j < G->vexnum;j++){
				if(G->vexs[j] == G->vexs[p_end]){
					G->vexs[j] = G->vexs[p_start];
				}
			}
		}
	}
	cout << "克鲁斯卡尔算法生成的最小生成树的各边为:" << endl;
	for(int i = 0;i < k;i++){
		cout << res[i].start << ", " << res[i].end << "权值为:" << res[i].weight << endl;
	}
}

int main()
{
	Graph G;
	init(&G);
	//show(&G);
	EData *edges;
	getEdges(&edges, &G);
	cout << "排序前:" << endl;
	showEdges(edges, &G);
	sort(&edges, &G);
	cout << "排序后:" << endl;
	showEdges(edges, &G);
	cout << "----" << endl;
	kruskal(&edges, &G);
	free(edges);
	return 0;
}

运行效果:
最小生成树算法:普利姆、克鲁斯卡尔(附详细代码)


普利姆的:

#include<iostream>
#include<cstdlib>
using namespace std;

#define MAX_INT 999	 //边的最大权值,视为正无穷 
#define MAX 100		//数组最大长度 
/*数据结构*
// 邻接矩阵
*/
typedef struct _graph
{
    char vexs[MAX];       // 顶点集合
    int vexnum;           // 顶点数
    int edgnum;           // 边数
    int matrix[MAX][MAX]; // 邻接矩阵
    int dis[MAX];		  //标记U集合到集合外各点的最小权值
	int src[MAX];	  	  //记录路径 
}Graph, *PGraph;

// 边的结构体
typedef struct _EdgeData
{
    char start; // 边的起点
    char end;   // 边的终点
    int weight; // 边的权重
}EData;

/*
**/

//初始化邻接矩阵 
void init(PGraph G)
{
	//所有顶点 
	G->vexs[0] = 'A';G->vexs[1] = 'B';G->vexs[2] = 'C';G->vexs[3] = 'D';G->vexs[4] = 'E';
	G->edgnum = 7;
	G->vexnum = 5;
	//填充邻接矩阵
	G->matrix[0][1] = 9;G->matrix[0][2] = 8;G->matrix[0][3] = MAX_INT;G->matrix[0][4] = MAX_INT;
	G->matrix[1][2] = 2;G->matrix[1][3] = MAX_INT;G->matrix[1][4] = 6;
	G->matrix[2][3] = 4;G->matrix[2][4] = 5;
	G->matrix[3][4] = 3;
	//根据对称性质
	for(int i = 1;i < G->vexnum;i++){
		for(int j = 0;j < i;j++){
			G->matrix[i][j] = G->matrix[j][i];
		}	
	} 
}

void show(PGraph G)
{
	cout << "图的邻接矩阵为:" << endl;
	for(int i = 0;i < G->vexnum;i++){
		for(int j = 0;j < G->vexnum;j++){
			if(G->matrix[i][j] == MAX_INT)
				cout << "#" << " ";				//无穷大用#表示 
			else
				cout << G->matrix[i][j] << " ";
		}	
		cout << endl;
	} 
}
//普利姆算法
int* prim(PGraph G, int *sum) {
	int* res = (int*)malloc(sizeof(EData) * G->vexnum);int k = 1;*sum = 0;	//res数组装每次选择加入最小生成树的结点序号 
	*res = 0;		//第一个节点作为初始节点 
	for(int i = 0;i < G->vexnum;i++){	//初始化dis数组 和 src数组 
		G->dis[i] = G->matrix[0][i];
		G->src[i] = 0;						
	}
	for(int i = 0;i < G->vexnum;i++){
		int min = MAX_INT, min_index = 0;
		//每次找出dis数组中的最小值,作为下一个节点
		for(int j = 0;j < G->vexnum;j++){
			if(G->dis[j] != 0 && G->dis[j] < min){
				min = G->dis[j];
				min_index = j;
			}
		} 
		//结果更新
		*sum = *sum + G->dis[min_index];
		G->dis[min_index] = 0;			//min_index结点就进入U集合了 
		*(res + k) = min_index;
		k++;
		//dis数组更新。根据这个最小值节点,松弛附近的权值
		for(int j = 0;j < G->vexnum;j++){
			//考虑到负权值的情况 
			if(G->matrix[min_index][j] < G->dis[j] && G->dis[j] != 0){
				G->dis[j] = G->matrix[min_index][j];
				G->src[j] = min_index;		//要更改路径 
			}
		} 
	}
	return res;
}

//打印最小生成树构建方案 
void showPath(int *res, PGraph G, int sum)
{
	for(int i = 0;i < G->vexnum;i++){
		if(i == 0){
			cout << "1.以" << G->vexs[*(res + i)] << "为初始结点" << endl; 
		}else{
			int node_index = *(res + i);
			int src_index = G->src[node_index]; 
			cout << i + 1 << ".连接结点" << G->vexs[node_index] << "与结点" << G->vexs[src_index] << endl;
		}
	} 
	cout << "生成的最小生成树代价和为:" << sum << endl;
}

int main()
{
	Graph G;
	init(&G);
	show(&G);
	int sum = 0;	//记录最小生成树的总和 
	int* res = prim(&G, &sum);
	showPath(res, &G, sum);
	free(res);
	return 0;
}

运行效果:
最小生成树算法:普利姆、克鲁斯卡尔(附详细代码)

相关标签: 图论