贪心算法与数据结构结合2——最小生成树问题:Prim算法

我曾经听说过很多人的梦想,但由于时间的消磨。这些梦想也就慢慢消退了;
我也听说过很多人侃侃而谈,最后却只是谈了谈而已。
人基本都是这样的,肯定都有梦想,但就是碍于时间和精力以及周遭变化原因未能继续追寻,继续前进了。
所以,如果你还想奋斗,还想追寻那种美好的憧憬。请一定要坚持下去,不要放弃!

本篇是贪心算法与数据结构结合的第二篇,本篇主要介绍最小生成树问题和以及解最小生成树问题的贪心算法——Prim算法

1、最小生成树

什么是生成树

设G是n阶连通图,那么T是G的生成树当且仅当T没有回路且有n-1条边。
如果生成树增加了一条不属于树的边,那么就会构成回路

就如这个图中的左边T是一棵生成树,现在加一条边e,使其构成回路,那么这个就不是个树了。

去掉圈中的任意一条边,就得到G中的另外一棵生成树T’

也就是说生成树的算法步骤是选择边;约束条件是不形成回路;截止条件是边数达到n-1

生成树在网络中有着重要应用

下面介绍最小生成树

什么是最小生成树

给定一个无向连通带权图G=(V,E,W),其中w(e)∈W是边e的权。
G的一棵生成树T是包含了G所有顶点的树,树中各边的权之和W(T)称为树的权,具有最小权的生成树成为G的最小生成树

我们看个例子:

我们看到啊左边的图是一个无向带权连通图,总共有10条边,E集合里面是连接这些边的两个端点的集合。右边是它的一棵最小生成树,我们可以通过计算得出,右边的这个生成树的权值之和是产生所有生成树里最小的一棵。

而对于求解最小生成树问题,我们可以用贪心法来解
这个贪心法有Prim算法和Kruskal算法。
我们首先看Prim算法

2、Prim算法

什么是Prim算法

输入:图G=(V,E,W),V={1,2,…,n}
输出:最小生成树T
1、初始将顶点1添加到集合S里
2、选择连接S与V-S集合最短边e={i,j},i∈S,j∈V-S,将e加入树T,j加入S
3、继续上述过程,直到S=V为止。

下面我们来看个例子:

下面我们用Prim算法来解:
初始将顶点1添加到S集合里

此时V-S={2,3,4,5,6},我们要再从V-S里面挑选一个点使其与顶点1构成最短边
从图中可以看到,顶点1与顶点2连接的边长度是6,顶点1与顶点3连接的边长度是1,顶点1与顶点4连接的边长度是5。显然那个长度是1最短。
因此我们把顶点3挑进来到集合S里

现在V-S={2,4,5,6}
现在因为3加到了S集合里,那么可以在S集合找到一个点,在V-S集合中找到一个点,使其两个点的连线距离最短。
我们看到,1和2,1和4分别是6和5;然而与顶点3相连的线段中最短的是3与6的连接(1和3已经都选到S里了,就不应再考虑顶点1和顶点3连接的线段了)
那么把6加到集合S里

然后再从V-S集合里面挑,发现6和4之间的连线长度2是最短的,那么我们把4添加到集合S里。

然后就剩下{2,5},从图中我们看到1和2,3和5长度都是6;而3和2的长度是5,那么我们把2添加到集合S里。

最后由于2和5距离最短是长度3,所以把5添加到集合S里。

当然上边两点连接的那个最小长度的边是要加到T里面作为最小权值。所以构成的最小生成树是这样的

将这些边上的权值加起来就是问题的最优解
此生成树的权值是15

那么这个Prim算法对不对呢,我们接下来用数学归纳法进行证明

数学归纳法证明Prim算法

证:对于任意k<n,存在一棵最小生成树包含算法前k步选择的边

当k=1时,存在一棵最小生成树T包含边e={1,i},其中{i,i}是所有关联1的边中权最小的。
我们要证明:算法选择的连接端点1的最小权值的边是正确的
如若不然,我们假设一棵最小生成树T,这个T不包含{1,i},然而现在我们要把这个{1,i}和T连接起来,将生成树构成一个回路
我们还是用上面那个例子说

左边是关连1的另一条边{1,j},j!=i。右边是连接{1,i}所构成了回路
现在我们用{1,i}替换{1,j}得到树T’

那么我们发现,由于{1,i}的权值比{1,j}的权值小,所以T’的权值<=T。如果T是一棵最优的话,那么T’也是一棵最优的树,因为T’的权值<=T。又因为T’包含了算法第一步的结果。所以归纳基础是正确的

下面进行归纳步骤:
假设算法进行了k步,存在一棵最小生成树包含了e1,e2,…,ek这些边,而且这些边的端点选到了集合S里。
下面分析第k+1步的情况,假设算法选到了{ik+1,ek+1},而对于剩下的点,ik+1与S中某顶点连接的ek+1权值最小。那么k+1步显然正确;如果算法没有选到{ik+1,ek+1},那么设这么一棵没有包含ik+1的最小生成树为T,那么就和归纳基础一样。将{ik+1,ek+1}和T连接起来,使得T构成了一个回路

右边的红线是{ik+1,ek+1}和T连接起来

下面我们用右边选择的替换左边那个线,得到了一棵新的最小生成树T’

那么由于{ik+1,ek+1}是权值最小的边,所以T’的权值是<=T。如果说T是一棵最小生成树的话,那么T’也是一棵最小生成树。而且T’包含了算法第k+1步选着的结果。故k+1步也是正确的,因此Prim算法是正确的。证毕。

而对于代码分析,大家有没有想过这个和上一篇的Dijkstra算法中的代码很类似。所以可以参考Dijkstra算法中的代码进行编写,这里就不再分析了。
直接上代码

public class Graph {
	int v1;  //v1顶点
	int v2;  //v2顶点
	int len;  //权值
	
	public Graph() {
		// TODO Auto-generated constructor stub
	}

	public Graph(int v1, int v2, int len) {
		super();
		this.v1 = v1;
		this.v2 = v2;
		this.len = len;
	}
	
	
}

public static void main(String[] args) {
		// TODO Auto-generated method stub
		int m=9;  //总共9个顶点
		//设置权重:自己和自己连的是0,没有边的是-1,其他的都是自己权重值
		int[][] a = {
                {0,10,-1,-1,-1,11,-1,-1,-1},
                {10,0,18,-1,-1,-1,16,-1,12},
                {-1,18,0,22,-1,-1,-1,-1,8},
                {-1,-1,22,0,20,-1,24,16,21},
                {-1,-1,-1,20,0,26,-1,7,-1},
                {11,-1,-1,-1,26,0,17,-1,-1},
                {-1,16,-1,24,-1,17,0,19,-1},
                {-1,-1,-1,16,7,-1,19,0,-1},
                {-1,12,8,21,-1,-1,-1,-1,0}
        };
	
	CreateGraph2(a, m);
				System.out.println("生成的最小生成树是:");
				prim(a, m);
	}

//创建图
	public static void CreateGraph2(int a[][],int m) {
		for(int i=0;i<m;i++)
			for(int j=0;j<m;j++)
				if(a[i][j]==-1||a[i][j]==0)
					a[i][j]=0x3f3f3f3f;
	}
	
	//贪心算法解最小生成树(普里姆算法)
	public static void prim(int a[][],int m) {
		int sum=0;
		int num=0;
		int pointnum=m;
		int min=0,k=0;
		Boolean p[]=new Boolean[m];  //判断哪些点取到了
		Graph minTempLen[]=new Graph[m];  
		Graph tracing[]=new Graph[m];  //最小生成树的解,最后一个放最小生成树的权值和
		
		minTempLen[0]=new Graph(1, 1, a[0][0]);
		p[0]=true;
		for(int i=1;i<m;i++) {
			minTempLen[i]=new Graph(1, i+1, a[0][i]);
			p[i]=false;
		}
		for(int i=1;i<m;i++) {
			min=0x3f3f3f3f;
			for(int j=0;j<m;j++)
				if(minTempLen[j].len<min&&!p[j]) {
					min=minTempLen[j].len;
					k=j;
				}
			p[k]=true;  //找到一个最小权值的点
			tracing[num++]=minTempLen[k];
			sum+=min;
			pointnum--;
			
			for(int j=0;j<m;j++) {
				if(!(p[j])&&a[k][j]<minTempLen[j].len) {  //如果此时的权值小于上一个点的权值,则添加进去
					minTempLen[j]=new Graph(k+1, j+1, a[k][j]);
				}
			}
		}
		tracing[m-1]=new Graph(0x3f3f3f3f, 0x3f3f3f3f, sum);
		
		if(pointnum==1) {
			TracingMST2(tracing);
		}
		else
		    System.out.println("不存在最小生成树");
	}
	
	//遍寻最小生成树的边
	public static void TracingMST2(Graph tracing[]) {
	    for(int i=0;i<tracing.length-1;i++) 
	    	System.out.println((tracing[i].v1-1)+"<-->"+(tracing[i].v2-1));
	    System.out.println("得到的最小生成树的权值和是:"+tracing[tracing.length-1].len);
	}
对Prim算法进行分析

我们看到这个代码是和Dijkstra算法中的代码很类似。也是第一层循环中包含第二层循环,等第二层循环介绍后再进行第三次循环。故时间复杂度也是O(n^2)。

同样的使用更巧妙的数据结构,可以将时间复杂度降下来。

而对于最小生成树的另一种方法Kruskal算法,我会放在下一篇进行介绍。

           古人学问无遗力,少壮工夫老始成。(陆游《冬夜读书示子聿》)
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值