《面向实验编程》-《图论》-<Kruskal与Prim>

针对我们学校的实验报告,写了这篇"日记"
我直接放题+代码,后面再逐步解析。
在这里插入图片描述

#include<iostream>
#include<vector>
#include<set>
#include<unordered_set>
#define autor_congCongcong
#define MAX_INTEGER_OF_GRAPH 99999
using namespace std;
typedef struct Graph {
	int edge[100][100];
	int vertexNumber;
	int edgeNumber;
}*Gra;
typedef struct Edge {
	int a, b;
	int weight;
	bool operator <(const Edge& a)const {
		return weight < a.weight;
	}
};
typedef struct animal {
	int number;
	int maxDist;
};
int Prim(Gra myGraph) {
	int pathSum = 0;
	//以0为根节点
	int* distance = new int[myGraph->vertexNumber];
	distance[0] = -1;//表示已经在树中
	for (int count = 1; count < myGraph->vertexNumber; count++)
		distance[count] = myGraph->edge[0][count];//初始化
	int cntOfCollected = 1;
	int minDistance,minDistanceIndex;
	while (true) {
		minDistance=MAX_INTEGER_OF_GRAPH ;
		minDistanceIndex = 0;
		for (int count = 1; count < myGraph->vertexNumber; count++)
			if (minDistance > distance[count]&&distance[count]!=-1) {
				minDistance = distance[count];
				minDistanceIndex = count;
			}
		//找最小值和他的下标
		distance[minDistanceIndex] = -1;//收录进树中
		cntOfCollected++;
		pathSum += minDistance;
		for (int count = 1; count < myGraph->vertexNumber; count++) {
			if (distance[count] != -1)
				if (distance[count] > myGraph->edge[minDistanceIndex][count])
					distance[count] = myGraph->edge[minDistanceIndex][count];
		}
		if (cntOfCollected == myGraph->vertexNumber)return pathSum;
	}
}
void Floyd(int **D,int sizeOfMatrix) {
	for (int k = 0; k < sizeOfMatrix; k++)
		for (int i = 0; i < sizeOfMatrix; i++)
			for (int j = 0; j < sizeOfMatrix; j++)
				if (D[i][j] > D[i][k] + D[k][j])D[i][j] = D[i][k] + D[k][j];
}
animal FindMaxDist(int* distanceArr, int animalNumber,int size) {
	animal a;
	a.number = animalNumber;
	int max = -1;
	for (int count = 0; count < size; count++) 
		if (max < distanceArr[count])
			max = distanceArr[count];
	a.maxDist = max;
	return a;
}
Gra CreateGraph(int vertexNumber) {
	Gra retGraph = new Graph;
	for (int count = 0; count < vertexNumber; count++)
		for (int i = 0; i < vertexNumber; i++)
			retGraph->edge[count][i]= MAX_INTEGER_OF_GRAPH;
	retGraph->vertexNumber = vertexNumber;
	for (int count = 0; count < vertexNumber; count++)
		retGraph->edge[count][count] = 0;
	return retGraph;
}
Edge CreateEdge(int a, int b, int weight) {
	Edge retEdge;
	retEdge.a = a;
	retEdge.b = b;
	retEdge.weight = weight;
	return retEdge;
}
void insertEdge(Gra myGraph, Edge insertOne) {
	myGraph->edgeNumber++;
	myGraph->edge[insertOne.a][insertOne.b] = insertOne.weight;
	myGraph->edge[insertOne.b][insertOne.a] = insertOne.weight;

}
void FindAnimal(Gra myGraph) {
	int numberOfVertex = myGraph->vertexNumber;
	int** martix = new int* [numberOfVertex];
	for (int count = 0; count < numberOfVertex; count++)
		martix[count] = new int[numberOfVertex];
	Floyd(martix, numberOfVertex);
	int minDistance = MAX_INTEGER_OF_GRAPH;
	int indexOfNumber=0;
	for (int count = 0; count < myGraph->vertexNumber; count++) {
		animal temp = FindMaxDist(myGraph->edge[count], count, numberOfVertex);
		if (temp.maxDist < minDistance) {
			minDistance = temp.maxDist;
			indexOfNumber = temp.number;
		}
	}
	cout << "The Animal is " << indexOfNumber << endl;
	cout << "It cost " << minDistance << endl;
}
int find(int* parrent, int a) {
	int finalParrent=a;
	while (true) {
		if (finalParrent != parrent[finalParrent])finalParrent = parrent[finalParrent];
		else break;
	}
	parrent[a] = finalParrent;
	//路径压缩
	return parrent[a];
}
void Union(int* parrent, int indexA, int indexB,int size) {
	int parrentA = find(parrent, indexA);
	int parrentB = find(parrent, indexB);
	if (parrentA != parrentB)
	{
		for(int count=0;count<size;count++)
			if(parrent[count]==parrent[indexA])
				parrent[indexB] = parrent[count];
	}
}
int Kruskal(Gra myGraph) {
	int pathSum = 0;//这是需要返回的路径和,我将其初始化为0
	multiset<Edge> minHeapToGetCurrentMinEdge;//这里建立一个最小堆(在上面的定义处我已经重载了运算符了),
	//而且是multiset,避免常出现权值一样的边而把另一条边忽略了
	int numberOfVertex = myGraph->vertexNumber;
	//定义节点总数的临时变量,避免后面多次调用graph里面的值
	for (int count = 0; count < numberOfVertex; count++)
		for (int j = count + 1; j < numberOfVertex; j++)
			minHeapToGetCurrentMinEdge.insert(CreateEdge(count, j, myGraph->edge[count][j]));
	//这里是一个对最小堆的初始化,把每一条边塞到这个最小堆中
	int* parrent = new int[numberOfVertex];
	//这个parrent数组是一个并查集的集合
	for (int count = 0; count < numberOfVertex; count++)
		parrent[count] =count;
	//初始化该并查集,让他的双亲节点为自己,也就是说现在每一个顶点单独为一个集合
	//并查集
	multiset<Edge>::iterator it = minHeapToGetCurrentMinEdge.begin();//迭代器
	Edge temp;//用来存储边得临时变量
	for (int cnt = 0; cnt < numberOfVertex-1; it++) {
		//无论是Kruskal算法还是Prime算法,有n个顶点,那么要生成一棵生成树,永远只需要n-1条边
		//这里得节点数为numberOfVertex,那么一共只需要找numberOfVertex-1次
		temp = *it;//取出当前迭代器的存放的边
		if (find(parrent, temp.a) != find(parrent, temp.b))
		{
			//这里的find条件一个是找出他们的最大上级,另一个目的是为了压缩路径
			Union(parrent, temp.a, temp.b,numberOfVertex);
			//普通的并集
			pathSum += temp.weight;
			//更新pathSum
			cnt++;
			//一共只需要n-1条边
		}
	}
	return pathSum;
}
int main() {
	Gra myGraph =CreateGraph(7);
	insertEdge(myGraph, CreateEdge(0, 1, 28));
	insertEdge(myGraph, CreateEdge(0, 5, 10));
	insertEdge(myGraph, CreateEdge(1, 2, 16));
	insertEdge(myGraph, CreateEdge(1, 6, 14));
	insertEdge(myGraph, CreateEdge(2, 3, 12));
	insertEdge(myGraph, CreateEdge(3, 6, 18));
	insertEdge(myGraph, CreateEdge(3, 4, 22));
	insertEdge(myGraph, CreateEdge(4, 5, 25));
	insertEdge(myGraph, CreateEdge(4, 6, 24));
	int primInterger = Prim(myGraph);
	cout <<"利用Prime算法算出来的最短路径的长度和为:"<< primInterger << endl;
	 
	cout << "利用Kruskal算法算出来的最短路径的长度和为:" << Kruskal(myGraph)<<endl;
}

先放代码再解释

首先从主函数里面一步一步讲吧。图有两种存储模式,邻接矩阵和邻接表这里我基于这是一个稠密图的情况下我使用了邻接矩阵来存储这个图。我用一个Graph变量包装了三个元素,一个是edge也就是我们的邻接矩阵,edge[i][j]代表着之间存在着一条边,由于这个实验的节点数只有7个,所以其实这里可以改成edge[7][7],(但这是早期我遗留下来的写法),所以就不更改了,该数组在i j两个索引下的值代表着该边在此有权无向图的权值,edgeNumber就是边的个数,后面在插入的时候需要更新(++),vertexNumber是节点的个数(这里按题意是7)

有了这些先置条件后,
我们创建一个图,调用了CreateGraph函数,该函数传进了一个7
这个7代表顶点数,我们先看CreateGraph函数

typedef struct Graph {
	int edge[100][100];
	int vertexNumber;
	int edgeNumber;
}*Gra;
Gra CreateGraph(int vertexNumber) {
	Gra retGraph = new Graph;
	for (int count = 0; count < vertexNumber; count++)
		for (int i = 0; i < vertexNumber; i++)
			retGraph->edge[count][i]= MAX_INTEGER_OF_GRAPH;
	retGraph->vertexNumber = vertexNumber;
	for (int count = 0; count < vertexNumber; count++)
		retGraph->edge[count][count] = 0;
	return retGraph;
}

由于用的是一个邻接矩阵,在这个函数中,初始化矩阵中的每个值赋值为MAX_INTEGER_OF_GRAPH(对角线除外,对角线赋值为0,表示该点到该点的距离为0),然后vertexNumber就是节点数,edgeNumber表示边的个数。
这里要说一下为什么初始化为一个大的整数,原因是这是一个有权图,矩阵上的每一个数代表着这两个下标所代表的节点之间的边的权值。而初始化为一个很大的数目的是为了表示这两个边之间存在着一条权值很大的边约等于我表示了这两个顶点之间不可达
返回的就是Graph的一个指针

下面就是若干条Insert语句
我们看一下insert的代码

void insertEdge(Gra myGraph, Edge insertOne) {
	myGraph->edgeNumber++;
	myGraph->edge[insertOne.a][insertOne.b] = insertOne.weight;
	myGraph->edge[insertOne.b][insertOne.a] = insertOne.weight;
}
Edge CreateEdge(int a, int b, int weight) {
	Edge retEdge;
	retEdge.a = a;
	retEdge.b = b;
	retEdge.weight = weight;
	return retEdge;
}

在InsertEdge的代码中其实就是将该邻接矩阵以对角线对称赋予权值
我们在主函数中首先调用CreateEdge函数创建一条边,再把该边通过insertEdge丢进图里面,这两个操作的很简单,就不说了。
下面是调用的Prim算法
下面是Prim算法的源代码

int Prim(Gra myGraph) {
	int pathSum = 0;
	//以0为根节点
	int* distance = new int[myGraph->vertexNumber];
	distance[0] = -1;//表示已经在树中
	for (int count = 1; count < myGraph->vertexNumber; count++)
		distance[count] = myGraph->edge[0][count];//初始化
	int cntOfCollected = 1;
	int minDistance,minDistanceIndex;
	while (true) {
		minDistance=MAX_INTEGER_OF_GRAPH ;
		minDistanceIndex = 0;
		for (int count = 1; count < myGraph->vertexNumber; count++)
			if (minDistance > distance[count]&&distance[count]!=-1) {
				minDistance = distance[count];
				minDistanceIndex = count;
			}
		//找最小值和他的下标
		distance[minDistanceIndex] = -1;//收录进树中
		cntOfCollected++;
		pathSum += minDistance;
		for (int count = 1; count < myGraph->vertexNumber; count++) {
			if (distance[count] != -1)
				if (distance[count] > myGraph->edge[minDistanceIndex][count])
					distance[count] = myGraph->edge[minDistanceIndex][count];
		}
		if (cntOfCollected == myGraph->vertexNumber)return pathSum;
	}
}

因为是生成树,所以无论是不是最小只需要n-1条边,也就是n个节点
所以用cntOfCollected记录当前的已经收录的节点数。我这里是把0作为默认的源节点,换句话说,我这棵树最初始化的根节点是0,所以cntOfCollected初始化为1
在初始化中我也定义了一个distance数组,这表示着距离。在该distance数组中,所包含的意思是,我这个节点,到这棵树的距离是多少,如果节点i(0<=i<=6)在这棵树内,那就已经收录了的节点,那我就把distance[i]=-1以表示这个点已经在里面了
因为0一开始就在树中,所以我的distance[0]一开始就是-1;
那distance数组不是所有节点到这棵树的距离嘛。
那我一开始初始化distance数组全部都为edge【0】【count】(1<=count<=6)
以用来表示每个节点跟0的直接距离(这里的直接距离表示只通过一条边就能到达0)

关键看while(true)的循环

while (true) {
		minDistance=MAX_INTEGER_OF_GRAPH ;
		minDistanceIndex = 0;
		for (int count = 1; count < myGraph->vertexNumber; count++)
			if (minDistance > distance[count]&&distance[count]!=-1) {
				minDistance = distance[count];
				minDistanceIndex = count;
			}
		//找最小值和他的下标
		distance[minDistanceIndex] = -1;//收录进树中
		cntOfCollected++;
		pathSum += minDistance;
		for (int count = 1; count < myGraph->vertexNumber; count++) {
			if (distance[count] != -1)
				if (distance[count] > myGraph->edge[minDistanceIndex][count])
					distance[count] = myGraph->edge[minDistanceIndex][count];
		}
		if (cntOfCollected == myGraph->vertexNumber)return pathSum;
	}

在循环的最开始,初始化minDistance为一很大的值,用于后面寻找最小值。我这里随便初始化了一个最小值的下标为0(其实不初始化这个东西也可以)
在下面的for循环中

for (int count = 1; count < myGraph->vertexNumber; count++)
			if (minDistance > distance[count]&&distance[count]!=-1) {
				minDistance = distance[count];
				minDistanceIndex = count;
			}

由于count=0(节点0)被我们作为最初放进去的节点,我们肯定他已经在树里面了,那我们每一次就不用讨论他了。
我们每次都在当前的distance数组里面找到一个不在树内的最小值,并锁定他的值和他的下标(注意这里判断条件多了一个distance[count]!=-1用来表示这个点不在树内)
minDistance锁定该最小值
minDistanceIndex锁定该最小值对应的下标

然后知道这两个就好办了,Prime和Kruskal都是贪心嘛,那就本次循环就拿这个节点,pathSum(带权路径和)更新(+=minDistance)
然后把这个节点收录就OK了
收录的标志就是该点的distance要变成-1

		distance[minDistanceIndex] = -1;//收录进树中
		cntOfCollected++;
		pathSum += minDistance;

紧接着一个很自然的,我们收录进去了,那就是说现在这个点,跟我们原来的树融合成一体,那我自然要更新distance数组,回到上面的定义,这个distance的值是每个节点到这棵树的直接距离,由于之前的都是最优解(保证最小),那我只需要根据收录的这个点,做更新就可以了。

我某个位置的distance要不要更新,取决于我这个新收录的点对于跟这个点相连的其他点的长度会不会比原来的distance小,如果小,那么其他节点就能通过这个新收录的点到达这棵树(更新),否则, 就代表着,我这个顶点到你这个点的距离,反而比原来我到这棵树的距离大了,那我为什么要走这条边啊,我直接走原来的边不就行了(不更新)
个人觉得上面这段文字是整个prim算法的核心,看懂了其他都可以不看了

for (int count = 1; count < myGraph->vertexNumber; count++) {
			if (distance[count] != -1)
				if (distance[count] > myGraph->edge[minDistanceIndex][count])
					distance[count] = myGraph->edge[minDistanceIndex][count];
		}

由于在一整个死循环里面。为了不让死循环的发生,要产生程序的出口,已经要返回该pathSum。综上述,当节点全部收录,也就是说cntCollected为总节点数的时候,那么就break然后return
我偷懒了,我直接在while循环里面return了,但是我个人还是建议break出去然后return的

if (cntOfCollected == myGraph->vertexNumber)return pathSum;

这里停一下,明天更Kruskal,赶课时去了。


回来更新了,先写完再修改。上面Prim已经改完了,Kruskal明天再改。


背景跟Prim算法一样,都是稠密图,都是邻接矩阵
讲Kruskal要讲一些先置的知识点。
分别有两个
1. 并查集
2. 最小堆
这两个不会详细讲,有兴趣的读者可以去看一下别人的文章,由于我是学生党时间比较少,而且关于STL的multiset源码也还没吃透,所以我就不误人子弟了
1.先讲并查集吧,
并查集是一种集合化思想,我们常常在一个图中需要将不同的节点组合到一起成为一个集合,那么我需要定义一个parent数组。假如我有7个节点,那我就parent里面就有7个元素。他的索引代表着他是第几个节点。如图

在这里插入图片描述
parrent里面的值初始化为他们的下标值。
每当我需要将两个集合取并集,那么我只需要将其中一个的parrent改为另外一个的值就可以了,例如:
我要把0 和5合成在一个集合里。我只需要让parrent[5]=0就OK了
当我们有{0,5} {1,2}这个集合的时候,我们通过5 和2并集的时候(也就是说parrent[5]=0,parrent[2]=1)
我们如果简单的将2的parrent也就是1,parrent[1]=5的话,就出大问题了。为什么
我们仔细看
这时候
parrent[0]=0
parrent[5]=0
parrent[1]=5
parrent[2]=1
那我每次找2的龙头老大,是不是要走四次才知道(当index=parent[index]就找到了)
这显然很不合理,我们做一个路径压缩就能改变这个问题。这个其实就是在每一次联合的时候,让他这个集合的所有元素的parrent直接赋值上那个龙头老大的下标就可以了
这里就是并查集的简介,可能说的很粗糙,有兴趣了解的读者可以去看看其他博主的博客。
2.multiset(可以理解成最小堆,但是容许有重复值)
底层就是一棵树,根节点比左右子树的任意一个节点的值都要小(最小堆)。所以他呈现的状态就是每次拿一个最小值出来,然后更新堆。所以遍历他,就能得到一个有序(升序)的序列(最小堆升序,最大堆降序),读者可以自己去看看STL中multiset的用法,这里详细展开可以单独开一篇了

怎么规定最大还是最小,只需要在重载<运算符看看你要升序还是降序,我的是降序,反过来就是升序。Kruskal算法的源码涉及到其他的函数,我一并拿下来

int find(int* parrent, int a) {
	int finalParrent=a;
	while (true) {
		if (finalParrent != parrent[finalParrent])finalParrent = parrent[finalParrent];
		else break;
	}
	parrent[a] = finalParrent;
	//路径压缩
	return parrent[a];
}
void Union(int* parrent, int indexA, int indexB,int size) {
	int parrentA = find(parrent, indexA);
	int parrentB = find(parrent, indexB);
	if (parrentA != parrentB)
	{
		for(int count=0;count<size;count++)
			if(parrent[count]==parrent[indexA])
				parrent[indexB] = parrent[count];
	}
}
int Kruskal(Gra myGraph) {
	int pathSum = 0;//这是需要返回的路径和,我将其初始化为0
	multiset<Edge> minHeapToGetCurrentMinEdge;//这里建立一个最小堆(在上面的定义处我已经重载了运算符了),
	//而且是multiset,避免常出现权值一样的边而把另一条边忽略了
	int numberOfVertex = myGraph->vertexNumber;
	//定义节点总数的临时变量,避免后面多次调用graph里面的值
	for (int count = 0; count < numberOfVertex; count++)
		for (int j = count + 1; j < numberOfVertex; j++)
			minHeapToGetCurrentMinEdge.insert(CreateEdge(count, j, myGraph->edge[count][j]));
	//这里是一个对最小堆的初始化,把每一条边塞到这个最小堆中
	int* parrent = new int[numberOfVertex];
	//这个parrent数组是一个并查集的集合
	for (int count = 0; count < numberOfVertex; count++)
		parrent[count] =count;
	//初始化该并查集,让他的双亲节点为自己,也就是说现在每一个顶点单独为一个集合
	//并查集
	multiset<Edge>::iterator it = minHeapToGetCurrentMinEdge.begin();//迭代器
	Edge temp;//用来存储边得临时变量
	for (int cnt = 0; cnt < numberOfVertex-1; it++) {
		//无论是Kruskal算法还是Prime算法,有n个顶点,那么要生成一棵生成树,永远只需要n-1条边
		//这里得节点数为numberOfVertex,那么一共只需要找numberOfVertex-1次
		temp = *it;//取出当前迭代器的存放的边
		if (find(parrent, temp.a) != find(parrent, temp.b))
		{
			//这里的find条件一个是找出他们的最大上级,另一个目的是为了压缩路径
			Union(parrent, temp.a, temp.b,numberOfVertex);
			//普通的并集
			pathSum += temp.weight;
			//更新pathSum
			cnt++;
			//一共只需要n-1条边
		}
	}
	return pathSum;
}

上面的find和union就是并和找操作,是并查集中的内容。可以当作封装好的函数,不需要管,用就完事了。
在一开始,我定义了一个multiset,我用这个集合去存储各个边,由于是最小堆,所以我能保证是升序的一个集合(也可以把边拿出来用快排)。
我每次拿出最小的(没拿过的边),如果满足条件(不形成回路)
什么?如何判断不形成回路?并查集的find操作找他的parrent是不是一个就完事了,如果一样,那就不能连,因为连了会有回路。
我从小到大拿,我保证每一次拿都符合贪心策略,那我的最小生成树就在我拿了有效次数(cnt==n-1)之后,我就可以退出去了,因为一棵生成树,只需要n-1条边。
注意这里一定要用multiset,我也因为一开始用了set而把权值一样的边给吃掉了(插不进去)导致出来的生成树一直不对

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值