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

快速解决克鲁斯卡尔和普里姆算法--C++实现

程序员文章站 2022-07-13 08:36:50
...

关于此算法的思想. 具体可以参照
最小生成树视频演示(bilibili上的)

此视频演示是我的头号参照, 大量文字叙述算法只会帮倒忙, 必要部分进行画图陈述

克鲁斯卡尔算法(有向图形式):

先将其封装为一个个的方法, 方便代码重用

/*
 从上至下的方法作用:
 */
int getIndexOfRepresentative(const int index);
// 递归获取该编号节点的根节点索引
void merge(const int index1, const int index2);
// index1编号的集合与index2的集合合并, index2的根节点为合并后的树的根节点
bool isLoop(const int index1, const int index2);
// 判断要连接的两点是否会构成回路

比如对下图进行getIndexOfRepresentative(1)的调用, 返回的将是4
对getIndexOfRepresentative(3), getIndexOfRepresentative(7)也是如此
快速解决克鲁斯卡尔和普里姆算法--C++实现快速解决克鲁斯卡尔和普里姆算法--C++实现首先, 我们先得有存储右侧edge的物品, 这个时候
先起一个定义 边的结构体!

struct edge {
	int begin;
	int end;
	int len;
};

至于输入, 排序代码放在最后. 现在, 我们假定已经做好了排序, 完成了图中所示的结构, 那么接下来继续下一步

N

个结点,

M

条给定的无向边
我们先定义好
totalLength, 用于存放路径的总长度
addedEdges用于存放已经加入的边数
我们依据规则, 便能写出如下代码:

	int N, M;
	int totalLength = 0;
	int addedEdges = 0;

	cin >> N >> M;
	edge *edges = new edge[M];
	...
	...
	...
	int i = 0;
	while (i != M)
	{
		if (!(isLoop(edges[i].begin, edges[i].end)))
		{
			merge(edges[i].begin, edges[i].end);
			addedEdges++;
			totalLength += edges[i].len;
			cout << edges[i].begin << " -> " << edges[i].end
				 << "  len:" << edges[i].len << endl;
			if (addedEdges == (N - 1)) { cout << totalLength << endl; return 0; }

		}
		i++;
	}

从edges数组中

取一条最短的边, 判断加入后是否会构成回路, 否, 则将第一个集合与第二个连接起来, addedEdges自增一次, totalLength增加一次==
如图. 将索引为

0 4

的结点连接起来时
快速解决克鲁斯卡尔和普里姆算法--C++实现我们先判断是否构成回路, 没有构成回路, 进行下一步.
先将0 指向 4, 而 4 指向 6这个根节点
为了让所有结点最终都指向6
需要对索引0往回找, 直到找到根节点1

快速解决克鲁斯卡尔和普里姆算法--C++实现那么修改顺序如下:
0 —> 5 修改为 0 —> 4
5 —> 1 修改为 5 —> 0
1 —> 1 修改为 1 —> 5
快速解决克鲁斯卡尔和普里姆算法--C++实现至此, 完成了连接.
(3是指向根节点1的, 所以1指向改变, 3随之改变)

#include<iostream>
#include<algorithm>

struct edge {
	int begin;
	int end;
	int len;
};

static int nodes[5000];

/*
 从上至下的方法作用:
 递归获取该编号节点的根节点索引
 index1编号的集合与index2的集合合并, index2的根节点为合并后的树的根节点
 判断要连接的两点是否会构成回路
 */
int getIndexOfRepresentative(const int index);
void merge(const int index1, const int index2);
bool isLoop(const int index1, const int index2);
bool compare(edge e1, edge e2) {
	return e1.len < e2.len;
}

int main(void) {
	using namespace std;

	int N, M;
	int totalLength = 0;
	int addedEdges = 0;

	cin >> N >> M;
	edge *edges = new edge[M];

	for (int i = 0; i < M; i++) {
		cin >> edges[i].begin >> edges[i].end;
		cin >> edges[i].len;
	}
	sort(edges, edges + M, compare);

	for (int i = 0; i < N; i++) {
		nodes[i] = i;
	}

	cout << endl;
	/*
	 至此: 已经对边的长度进行了升序排序
	 完成了边数据的输入, 以及各顶点的指向初始化, 各个顶点看成各棵树上的根结点 
	 */
	int i = 0;
	while (i != M)
	{
		if (!(isLoop(edges[i].begin, edges[i].end)))
		{
			merge(edges[i].begin, edges[i].end);
			addedEdges++;
			totalLength += edges[i].len;
			
			cout << "第" << i + 1 << "次, 选择了边: "
			     << edges[i].begin << " -> " << edges[i].end
				 << "  权值:" << edges[i].len << endl;
				 
			/* 选择了N-1条边, 退出程序并且输出结果 */	 
			if (addedEdges == (N - 1)) 
			{ 
			cout << "路径总长度: " << totalLength << endl; 
			for (int i = 0; i < N; i++) 
				cout << i << "号节点的父节点是" << nodes[i] << "节点\n";
			return 0; 
			}
		}
		i++;
	}

	cout << "orz\n";
	return 0;

}
int getIndexOfRepresentative(const int x)
{
	if (nodes[x] == x) { return x; }
	else { return getIndexOfRepresentative(nodes[x]); }
}

/*
 将num1的代表结点指向num2的代表结点
 */
void merge(const int num1, const int num2)
{
	int next = num1;
	int front = num2;
	int temp = 0;
	
	while (nodes[next] != next)
	{
		temp = nodes[next];
		nodes[next] = front;
		front = next;
		next = temp;
	}
	nodes[next] = front;
}

/*
 传入将要连接的两个结点编号
 判断是否是形成了一个环
 */
bool isLoop(const int num1, const int num2)
{
	return getIndexOfRepresentative(num1) == getIndexOfRepresentative(num2);
}

对视频中数据进行测试
9 14
6 7 1
2 8 2
5 6 2
0 1 4
2 5 1
6 8 6
7 8 7
2 3 7
0 7 8
1 2 8
3 4 9
4 5 10
1 7 11
3 5 14
得到快速解决克鲁斯卡尔和普里姆算法--C++实现绘制如图
快速解决克鲁斯卡尔和普里姆算法--C++实现弊端:不能回退, 考虑进行双向链表进行实现.

struct node{
		int index;
		node* next;
		node* front;
};

普里姆算法(无向图形式)

离散思想:快速解决克鲁斯卡尔和普里姆算法--C++实现

按照套路, 这次我在代码中增加了提示输入的文字(可以删除), 我们和上面一样, 先进行数据的输入, 然后所有的边全部被升序排序

与克鲁斯卡尔算法依照边的一次while循环不同, 我们不能查看一条边(不管使用还是不使用)都跳过这条边, 因为一条边权值很小但是可能当前用不到----不代表之后用不到
我们在edge的结构体上进行改动:

/* isUsed初始化为false, 已经连接的边, 会被设置为true, 避免某条边重复使用 */
struct edge {
	int node1;
	int node2;
	int length;
	bool isUsed;
};

我们使用一条边的依据是:
边的两个结点序号有且至多有一个存在于已经连接的结点中, 说明这条边可以使用

全部代码:

/* 获取下一个要连接的结点的序号(0 ~ N-1) 
 * 判断方法: e的两个结点序号有且至多有一个存在于nodes数组中
 * 这样正好保证了不会形成回路
 */
int nextNode(int* nodes, int currentNodes, edge e){
	int index1, index2;
	index1 = index2 = -1;

	for (int i = 0; i < currentNodes; i++) {
		if (e.node1 == nodes[i]){ index1 = i; }
		else if (e.node2 == nodes[i]){ index2 = i; }
	}
	if ((-1 == index1 && index2 == -1) || (-1 != index1 && index2 != -1)) { return -1; }
	else { return index1 == -1 ? e.node1 : e.node2; }
}
#include<iostream>
#include<algorithm>

/* isUsed初始化为false, 已经连接的边, 会被设置为true, 避免某条边重复使用 */
struct edge {
	int node1;
	int node2;
	int length;
	bool isUsed;
};

/* 用于sort()函数按照权值大小升序排序edge边数组
 * 当e1 和 e2 的位置反过来时, 会变成降序排序
 */
bool compare(edge e1, edge e2) {
	return e1.length < e2.length;
}

/* 获取下一个要连接的结点的序号(0 ~ N-1) 
 * 判断方法: e的两个结点序号有且至多有一个存在于nodes数组中, 这样正好保证了不会形成回路
 */
int nextNode(int* nodes, int currentNodes, edge e){
	int index1, index2;
	index1 = index2 = -1;

	for (int i = 0; i < currentNodes; i++) {
		if (e.node1 == nodes[i]){ index1 = i; }
		else if (e.node2 == nodes[i]){ index2 = i; }
	}
	if ((-1 == index1 && index2 == -1) || (-1 != index1 && index2 != -1)) { return -1; }
	else { return index1 == -1 ? e.node1 : e.node2; }
}

int main(void)
{
	using namespace std;

	/* 准备工作 */
	int N, M;

	cout << "结点个数: ";
	cin >> N;
	cout << "给定的边数: ";
	cin >> M;

	int* nodes = new int[N] {};
	edge* edges = new edge[M];
	cout  << "请依次输入边: 结点序号1 结点序号2 权值\n";
	for (int i = 0; i < M; i++) {
		int node1, node2, len;
		cin >> node1 >> node2 >> len;
		edges[i].node1 = node1;
		edges[i].node2 = node2;
		edges[i].length = len;
		edges[i].isUsed = false;
	}
	sort(edges, edges + M, compare);

	/* nodes数组存储已经连接好的结点, 在无向图中, 只需要将已经连接的结点看成整体
	   然后每次选择最小权值的边, 当currentNodes等于结点总数, 说明最小生成树完成
	   nodes[0]意味着先将第一个结点看成起始点 */

	nodes[0] = 0;
	int totalLength = 0;
	int i = 0;
	int currentNodes = 1;
	while (currentNodes != N)
	{

		while(i < M){
			if (edges[i].isUsed == false)
			{
				int temp = nextNode(nodes, currentNodes, edges[i]);
				if (-1 != temp) { 
					nodes[currentNodes++] = temp;
					edges[i].isUsed = true;
					totalLength += edges[i].length;
					cout << edges[i].node1 << " -- " << edges[i].node2 << endl;
					break;
				}
			}
			i++;
		}
	}
	cout << "总权值: "<< totalLength << endl;

	return 0;
}

运行结果:
快速解决克鲁斯卡尔和普里姆算法--C++实现快速解决克鲁斯卡尔和普里姆算法--C++实现
分析:
以0号结点为整体, 循环从edges数组中找到包含0节点的边, 且权值最小------------是 0 1 4, 连接, 然后将isUsed标记为true,然后将新节点1加入到nodes数组中
现在以0 , 1 为整体, 从edges数组中寻找包含0, 1其中之一结点的边, 且权值最小-------是0 7 8, 连接, 然后将isUsed标记为true, 将新结点加入到nodes数组中,
现在以0, 1, 7结点为整体…

至此完成~~;