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

本篇介绍解决最小生成树问题的另一个贪心算法:Kruskal算法

Kruskal算法

什么是Kruskal算法

输入:图G=(V,E,W)
输出:G的最小生成树T

1、按照长度从小到大排序
2、以此考察当前最短边e,如果e与T的边不构成回路,则把e加入树T,否则跳过e,直到选择了n-1条边为止

下面我们看个例子:
还是以上一篇那个实例进行说明

首先对边进行从小到大排序,然后考虑当前最短边。从图中看长度为1是最短边,所以把那个长度为1的边加到T中

然后再从剩下的边中进行选择,找到了最短的边长度为2。将这个边加入到T中

再从剩下的边中找到了最短的边长度为3,将这个边加入到T中

同样的,将长度为4的边加入到T中

最后我们考虑长度为5的边,我们看到如果连接3,4则3,4,6会构成回路;如果连接1,4则1,3,6,4会构成回路。那么只能连接2和3。将这条边加入到T中

至此红色的边及其顶点所构成的就是一棵最小生成树。

下面用数学归纳法证明Kruskal算法

用数学归纳法证明Kruskal算法

对于任意n,算法对n个顶点构成的图(n阶图)找到一棵最小生成树
n=2时,两个顶点只能连成一条边,即G中只有一条边。最小生成树就是G。所以归纳基础是正确的。

下面进行归纳步骤
假设算法对于n阶图是正确的,证明对于n+1阶图,算法也能找到一棵最小生成树
证明如下:
我们对任意的n+1阶图G,将其中含有最短边e的两个顶点合二为一(这种合二为一的操作称作短接操作),得到一个n阶图G’

我们上面的图是短接之前的图,即n+1阶图,然后将最短边e的两个顶点合二为一,就成为了下面那个图,当然两个点分别相连的边,经合二为一后变成一个点与这些边相连。
那么对于下面的这个n阶图来说,可以用归纳假设让算法找到一个最小生成树T’。

然后我们再拉伸回来,让那个合二为一的点分开成两个点

分开之后,刚才由一个点连的边再还回去,得到这一个生成树T
下面证明T是一棵最小生成树
如若不是,存在一棵最小生成树T1,这个T1是包含e的(如果e不在T’里面,那么可以加上边e构成回路,去掉回路中任意别的边所得的生成树的权仍旧最小)。那么就会有T1的权值小于T

下面我们对T和T’通过短接e的操作,将e这条边去掉
T1-{e}<T-{e}=T’
也就证明出,这个T1短接e后的权值是小于T’,这就会与刚才通过归纳假设证明的T’的权值最小产生了矛盾。故不存在这个T1。T就是n+1阶图的最小生成树,所以Kruskal算法是正确的。
证毕!

下面我们分析代码:
首先按照权值进行升序排序,然后就是判断两个点是否可以连接。
我们可以通过建立一个数组a,初始化时:a[i]=i,这个表示的是将每一个点设为根。
连接边时:是要先判断两个点是不是属于同一个根元素点。就是说顶点1与顶点4连接,我们根据小的顶点把大的顶点作为根结点。我们看到这个图

比如说:1与3连接,根据小的顶点把大的顶点作为根结点为准则,那么a[1]=3。也就是说3作为根连接1这个点。然后4和6相连,同理我们把a[4]=6。不管2和5相连,3和6相连时,a[3]=6,随后更新a[1]=6。如果遇到这种父结点的根改变了,那么子节点的根结点也要随着父结点一起改变。
接下来看1和4连接。因为a[1]=6,a[4]=6。他们两个的根都是6。故不可以相连,因为此时相连会产生回路。同样的3和4也不能相连。因为a[3]=6,a[4]=6。这样的拥有同一个根结点,则两顶点是不能相连的,因为会产生回路。这样的算法是在《算法导论》中有证明,而且也证明了,建立和更新a数组的时间复杂度是O(eloge)。大家有兴趣的可以去看一看。

至于a[i]的更新。比如说a数组中是{1,2,3,4,5,6}。那么我想找到顶点1的根结点是多少,我们可以通过回溯原理进行寻找。我们将查找算法写成find(int i),i=find(a[i])
我们举个例子:假如说1和5连接。1的根结点是4,而在其他连接中4的根结点是9,那么寻找1的根结点就得是i=a[1]=4,然后再回溯a[i]=9。进而找到1的根结点。

如果想不透的可以将a数组定义成连通分支标记。如果标记一样的,则不能连接

大家或者可以通过代码再想想

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}
        };
		
			    Graph tempGraph[]=CreateGraph(m, a);
			    
				//按权值进行升序排序
				Arrays.sort(tempGraph,new Comparator<Graph>() {
					public int compare(Graph o1, Graph o2) {
						return o1.len-o2.len;
					};
				});
				kruskal(tempGraph, m, tempGraph.length);
		}
//贪心算法解最小生成树(克鲁斯卡尔法)
	public static void kruskal(Graph tempGraph[],int m,int k) {
		int numPoint=m;  //未结点数
		int v1;  //访问点1
		int v2;  //访问点2
		List<Graph> minST=new ArrayList<Graph>();  //寄存选取的最小生成树的边
		
		int a[]=new int[m];  //将根记录下来
		
		//将每个点设为一个根
		for(int i=0;i<m;i++)
			a[i]=i;
		
		for(int i=0;i<k&&numPoint>1;i++) {
			v1=tempGraph[i].v1-1;
			v2=tempGraph[i].v2-1;
			
			//判断两个点是不是属于同一个根元素点
			if(UnitRoot(v1, v2, a)) {
				minST.add(tempGraph[i]);
				numPoint--;
			}
		}
		
		if(numPoint==1)
		    TracingMST(minST);
		else
			System.out.println("不存在最小生成树");
	}
	
	//访问根节点
	public static int FindRoot(int a[],int root) {
		if(root==a[root])
			return root;  //如果访问点就是根则直接返回
		return a[root]=FindRoot(a, a[root]);  //否则递归找到访问节点的根元素
	}
	
	//将根元素合并
	public static Boolean UnitRoot(int root1,int root2,int a[]) {
		int temp1=FindRoot(a, root1);
		int temp2=FindRoot(a, root2);
		if(temp1!=temp2) {
			a[temp2]=temp1;  //若两个节点根元素不一样,则将标号大的点作为根赋给标号为小的点
			return true;
		}
		else
			return false;  //若两个节点根元素一样,则说明这两个点再连接就会是一个闭合的图,而不再是树
	}
	
	//遍寻最小生成树的边
	public static void TracingMST(List<Graph> minST) {
		int sum = 0;
		System.out.println("生成的最小生成树是:");

	    for (int i=0;i<minST.size();i++) {
	        System.out.println((minST.get(i).v1-1)+"<-->"+(minST.get(i).v2-1));
	        sum+=minST.get(i).len;
	    }
	    
	    System.out.println("得到的最小生成树的权值和是:"+sum);
	}
	
	//创建图
	public static Graph[] CreateGraph(int m,int a[][]) {
		int k=0;
		Graph graph[]=new Graph[m*a.length];
				for(int i=0;i<m;i++)
					for(int j=0;j<i;j++) {
						if(a[i][j]>0) {
						graph[k]=new Graph(i+1,j+1,a[i][j]);
						k++;
						}
					}
				
				Graph tempGraph[]=new Graph[k];
				for(int i=0;i<k;i++)
					tempGraph[i]=graph[i];
				return tempGraph;
					
	}

下面对Kruskal算法进行分析

Kruskal算法分析

令e为边数
我们在上面说了建立和更新数组是O(eloge)
排序算法的时间复杂度是O(eloge)
而一层for循环中的遍历e条边的时间复杂度是O(e)

所以Kruskal算法的时间复杂度就是O(eloge)
以上就是Kruskal算法的内容介绍

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值