算法设计与分析——prim算法

目录

  • 前言
  • 一、算法思想分析
  • 二、算法效率分析
  • 三、算法代码
    • C语言代码
  • 后记

前言

在上一篇文章中,我们聊了聊KMP算法,一个极其高效但又非常难以理解(个人看来)的算法,如果有朋友想要深度讨论,欢迎私信。
本篇我们来聊聊prim算法,我最早接触prim算法已经记不清是数据结构课中还是离散数学课中了,但即便科目不同,prim算法还是那个prim算法。
prim算法是一种用于在连通图中获取最小生成树的算法同样用于获取最小生成树的算法还有Kruskal算法(大家有兴趣同样可以了解了解,但其实二者是比较相似的),prim算法属于贪心算法的一种,至于为什么,或许在分析其过程的时候大家就明白了。
要理解prim算法,大家首先要理解好什么是连通图?什么是最小生成树?这里我们来简单介绍一下,因为这不是我们本次的重点(如果已经有基础的朋友,可以适当跳过)。

  • 什么是连通图?,首先理解什么是图?(这里面的学问其实有点大,所以我说咱们就是简单介绍下哈,本篇文章还是对有基础的朋友较友好,因为对于有基础的朋友,我可能是在废话哈哈哈)简单来说,平面内许多点通过一些路径相互连接(不一定要全部连接),就构成了一个图,而根据这些点之间的路径是否有方向,又分为了无向图和有向图。如下图,分别是一种无向图和有向图。
    算法设计与分析——prim算法_第1张图片
    那么?什么是连通图呢?简单来说(只能简单来说了,说多了可以另起一篇文章。。。),就是图中的点通过图中的某些路径(带方向的就必须按照方向来)可以抵达任意一个点。最为简单的模型就是一个地区的许多村落之间的通信,如果每个村庄之间都可以达成通信,就是连通的。

  • 最小生成树是什么呢? 连通图中,每条路径都存在一个权值(理解为距离/成本也行吧,但其实不是这么理解的)。从该连通图出发,寻找一个路径数最少,但每个点之间都可达,就是一个生成树了(专业术语是连通无环子图)。生成树不止一个,而所以生成树中权值之和最小的生成树称为最小生成树。

以上是对最小生成树的简单介绍,如果大家想详细了解一下,可查询资料,这些都不是我们本次的重点。如下给出《算法设计与分析基础》中生成树和最小生成树的定义。
生成树和最小生成树定义
如下为一些例子:
算法设计与分析——prim算法_第2张图片

一、算法思想分析

在简单讲解最小生成树的概念后,我们来聊聊prim算法的思想。前面提到过,prim算法是贪心算法的一种,而贪心算法,讲究的就是要极力满足当前最优,这个当前最优正是prim算法的核心思想。
首先,prim算法中存在两个存储空间, 一个用来存放已加入最小生成树的顶点,而另外一个则用来存放还未加入最小生成树中的顶点。 当连通图中所有顶点已放入用于最小生成树的空间中,那么我们最小生成树算法结束!

  • 算法开始,起始顶点可随机找或指定一个。将起始顶点先放入已选顶点集合中
  • 未放入已选顶点集合中查找一条已选顶点集合中所有点最短(或者是权值最小,这里看自己理解了)的路径(说是一条线或许更合适吧)。刚开始的话,我们已选顶点集合只有一个点!而之后我们会将未加入的顶点一个一个加入已选顶点集合,所以这里的条件必须是已选顶点集合中所有点。显然,在我们找到的这条最短(权值最小)路径的两端的顶点,一个肯定属于还未加入最小生成树中的顶点,另一个肯定属于已加入最小生成树的顶点。将还未加入已选顶点集合的那个点,加入已选顶点集合,本轮任务完成。如果需要路径的,可以将路径记录保存下来。
  • 重复第二步,直到所有顶点已加入已选顶点集合,那么,我们的最小生成树就完成了。如果需要计算最小生成树的权值之和,在每一次找到最短路径的时候求一次和即可,需要具体路径的进行标记即可。

接下来,我们举个栗子来做个示范:
算法设计与分析——prim算法_第3张图片
算法设计与分析——prim算法_第4张图片
算法设计与分析——prim算法_第5张图片
算法设计与分析——prim算法_第6张图片
算法设计与分析——prim算法_第7张图片
算法设计与分析——prim算法_第8张图片
该例后续代码将会用到,大家可以自行看看该例。

二、算法效率分析

prim算法的具体效率,是与图的顶点和边数都是有关的。《算法设计与分析基础》中是这么分析的:
算法设计与分析——prim算法_第9张图片
以上是一种高级的分析,我们就简单来点吧。
假设总共又n个顶点,那么我肯定有n次比较过程!而在第k次比较过程中,又有n-k个点和另外n-k个顶点相互比较,那么总结起来,那么算法复杂度几乎再n的立方级别。或许有朋友问了,这么算起来,好像也不是很高效哦?n³级别复杂度的确不是很高,但其实prim算法真正效率是介于n²和n³之间的,因为我们如上的分析是一种最坏的情况,就是每个点之间都存在一条路径。
再者,相比于穷举法(也就是蛮力法)来查找最小生成树,prim算法已是大大提升了。《算法设计与分析基础》中这样写到:
prim算法和穷举法比较

三、算法代码

C语言代码

这里我们图的表示用邻接矩阵来表示,具体请看代码极其注释:

/*最小生成树 prim算法 从一个连通图中获取一个最小生成树*/
/*
输入要求:输入一个连通图 存储模式:邻接矩阵表示 0表示不可达  点依次默认命名为a,b,c,d,e,f,g...
例如:n*n数组
6
0 1 0 0 6 5
1 0 1 0 0 4
0 1 0 6 0 4
0 0 6 0 8 5
6 0 0 8 0 2
5 4 4 5 2 0
即为一种输入 实际上为上课讲解时的连通图表示
输出要求: n-1长度数组 包含n-1条路径 ()表示的为m号点到k号点的那条路径 
(1,2) (2,3) (2,6) (6,5) (6,4)
最小生成树权值之和:1+1+4+5+2=13
*/
#include
#define MAXN 1000
/*二维数组存储 采用邻阶矩阵数据*/
int input[MAXN][MAXN];
int sign[MAXN]= {0}; // 标记sign数组 初始化全为0 ,1表示已经加入已选列表
int prime(int n) {
	int sum=0; // 最小生成树权值之和 初始值为0
	/* 默认从第一个点开始 */
	sign[1]=1;
	int counter=1; // 计算当前状态已加入最小生成树队列的个数
	
	int flagX=-1,flagY=-1,minNodeValue=1000000;
	while(counter!=n) {
		flagX=-1,flagY=-1,minNodeValue=1000000; // 每一次都要初始化 
		/* 每一次循环都是一次贪心 当前局势的一种最优解 */
		for(int i=1; i<=n; i++) {
			if(sign[i]==0) continue; // 如果当前为加入队列 直接下一次循环 其实这是一个笨方法
			for(int j=1; j<=n; j++) {
				// 在查找下一个连接点时 发现这个连接点已经在里面了(或者为0,即不可达) 我们直接跳过 我能说这是一个笨方法么?但似乎只能这样
				if(sign[j]==1||input[i][j]==0) continue;
				if(input[i][j]<minNodeValue){
					// 更小的一个进行标记
					flagX=i;
					flagY=j;
					minNodeValue=input[i][j]; 
				}
			}
		}
		
		/* 一轮查找后 */
			sign[flagY]=1; // 谁该标1 要清楚
			counter++; // 找到加一 
			sum+=input[flagX][flagY]; // 这里其实不用担心指针越界 如果是连通图 在一轮查找下来 肯定能找到一个最小的 
			printf(" (%d,%d) ",flagX,flagY); // 其实我们都知道 flagX flagY 的位置 在这里顺序不重要 
	}
	return sum;
}
int main() {
	/*作为输入的主函数*/
	int n;
	printf("请输入邻接矩阵的维度n:"); 
	scanf("%d",&n);
	printf("邻接矩阵:\n");
	for(int i=1; i<=n; i++) {
		for(int j=1; j<=n; j++) {
			scanf("%d",&input[i][j]);
		}
	}
	/*调用函数*/
	int sum = prime(n);
	printf("\n最小生成树权值之和:sum=%d\n",sum);
}

代码中有部分存在取巧,如果大家有任何建议都可私信或留言。

后记

经过一番分析,prim算法的效率似乎是接近n³的,可能偏离了大家的预期。但是一个算法的高效,不是在于它的算法复杂度是多少,而是在于这个算法和解决同样问题的其他算法,相比之下,这个算法是否高效。
简而言之,高效算法是相对的,并不是绝对的。如果在解决最小生成树问题有其他算法比prim算法更为高效,那么那个算法也可以称之为高效算法。
以上只是我的理解,如果大家有其他意见和想法,欢迎留言或私信。

你可能感兴趣的:(算法设计与分析)