目录

1 问题描述

2 解决方案

2.1 构造最小生成树示例

2.2 伪码及时间效率分析

2.3 具体编码(最佳时间效率)

 


1 问题描述

何为Kruskal算法?

该算法功能:求取加权连通图的最小生成树。假设加权连通图有n个顶点,那么其最小生成树有且仅有n - 1条边。

该算法核心思想:从给定加权连通图中,选择当前未被选择的,不能形成回路且权值最小的边,加入到当前正在构造的最小生成树中。


2 解决方案

2.1 构造最小生成树示例

下面请看一个具体示例:

给定一个加权连通图,其包含5个顶点,分别为:1,2,3,4,5包含7条边,按照从小到大排序依次为:

1-2,5

2-3,5

3-5,6

2-4,12

4-5,12

2-5,15

3-4,17

那么可知,使用kruskal算法构造该加权连通图的最小生成树,则需要选择出这7条边中满足定义的4条边。

(1)原始图

(2)添加第1条边

此时未选中任何一条边,那么直接选择7条边中最小的一条边,2-3,5(PS:当权值最小的边有多个时,只要满足定义,可以随意选择一条边即可。例如,此处也可以选择1-2,5

(2)添加第2条边

此时,从剩余的6条边中选择最小权值的边,可以轻易知道为1-2,5加入此边后,检查此时的正在构造的最小生成树,没有回路,符合定义,即可以确认加入。

(3)添加第3条边

此时,从剩余的5条边中选择最小权值且不会生成环的边,轻易可知,3-5,6符合要求

(4)添加第4条边(PS:此时也是最小生成树的最后一条边)

剩余的4条边中选择最小权值且不会生成回环的边,发现2-4,12、4-5,12均符合要求,此时,任意选择其中一条边即可。这里,我选择的是4-5,12。

(5)最小生成树以及构造完毕,结束构造。

2.2 伪码及时间效率分析

该算法在开始的时候,会将给定连通图所有边的权值进行从小到大排列。然后,从一个空子图开始,它会扫描这个这个有序列表,并试图把列表中的下一条边加入到当前正在构造的子图(或者说是最小生成树)中。当前,这种添加不能形成一个回路,如果产生了回路,则把这条边跳过。

    Kruskal(G) {
//构造最小生成树的Kruskal算法
//输入:加权连通图G = <V, E>,其中V为顶点数,E为具体边集合
//其中E中边已经经过处理,按照权值从小到大排列
//输出:Et,组成G的最小生成树的边的集合
Et = 空集;
int count = 0; //用于计算进行已构造的边的总数
int k = 0; //表示从E中第一条边序号
while(count <= V - 1) {
k = k + 1;
if (Et U {ek}) { //集合Et加入第k条边不产生回路
Et = Et U {ek};
count++;
}
}
return Et;
}

通过以上的伪码,可以知道,Kruskal算法的时间效率取决于两点:

(1)对给定连通图所有边权值进行排序的时间效率;

(2)对新加入边,进行是否形成回路判断的时间效率。

首先,谈谈(1)的时间效率。对于排序算法,一般的时间效率分为O(n^2)(例如,选择排序和冒泡排序)和O(nlogn)(例如,合并排序和快速排序)。由于合并排序,相对于快速排序要稳定,所以,此处我们可以选择合并排序来处理问题(1,即时间效率为O(nlogn),其中n为顶点总数

其次,谈谈(2)的时间效率。对于问题(2)中要实现的功能,有一些高效的算法可以实现这种功能,这些算法的核心就是对两个顶点是否属于同一棵树的关键性检查,它们被称为并查算法,而该算法能够达到的最优时间效率为O(eloge),其中e为具体边总数。

 

最后,我们来探讨一下使用并查算法实现(2)中要求的功能。

使用并查算法实现检查回环问题,这里涉及的是一种抽象数据类型,这种数据类型是由某个有限集的一系列不相交子集以及下面这些操作构成的。

  1. id(x)生成一个单元集合{x}。假设这个操作对集合S的每一个元素只能应用一次。
  2. find(x)返回一个包含x的子集。
  3. union(x, y)构造分别包含x和y的不相交子集Sx和Sy的并集,并把它添加到子集的集合中,以代替被删除后的Sx和Sy。

例如,其中S = {1, 2, 3, 4, 5, 6}。首先使用id(x),初始化结果:{1},{2},{3},{4},{5},{6}。

现在执行union(1,4)得到{1,4},执行union(4,5)得到{1,4,5},此时集合结果:{1,4,5},{2},{3},{6}。

那么此时执行find(1)或者find(4)或者find(5)返回子集{1,4,5},执行find(3)返回子集{3}。

上面就是并查算法的应用思想,那么影响并查算法的时间效率,就是id(x)和union(x)函数的具体实现来决定。

此处对于id(x)和union(x)的实现,我采用树的性质来实现,把已经构造的边形成一棵树,当有新增的边时,且新增的边所在树的层数或者所有节点总数小于当前构造的树,那么我们就把新增的边所在树的根节点改变成当前正在构造的树的根节点的直接子节点。

例如合并子集{1,4,5,2}(PS:该子集构成的树根节点为1)}和{3,6}(PS:该子集构成的树的根节点为3),那么可以把根节点3直接转换为1的一个直接子节点即可。具体如下图所示:

讲完上面的定义及思想,下面就来具体看看对于2.1中示例图实现的编码应用。

首先,是初始化id(x),这里我首先令每一个单节点树的id(x)值为-1。

    for(int i = 0;i < n;i++)
id[i] = -1; //初始化id(x),令所有顶点的id值为-1,即表示为根节点

然后,是find(x)的实现:

//获取节点a的根节点编号
public int find(int[] id, int a) {
int i, root, k;
root = a;
while(id[root] >= 0) root = id[root]; //此处,若id[root] >= 0,说明此时的a不是根节点,因为唯有根节点的值小于0
k = a;
while(k != root) { //将a节点所在树的所有节点,都变成root的直接子节点
i = id[k];
id[k] = root;
k = i;
}
return root;
}

最后,是union(x)的实现:

//判断顶点a和顶点b的根节点大小,根节点值越小,代表其对应树的节点越多,将节点少的树的根节点作为节点多的树的根节点的直接子节点
public void union(int[] id, int a, int b) {
int ida = find(id, a); //得到顶点a的根节点
int idb = find(id, b); //得到顶点b的根节点
int num = id[ida] + id[idb]; //由于根节点值必定小于0,此处num值必定小于零
if(id[ida] < id[idb]) {
id[idb] = ida; //将顶点b的根节点作为顶点a根节点的直接子节点
id[ida] = num; //更新根节点的id值
} else {
id[ida] = idb; //将顶点a的根节点作为顶点b根节点的直接子节点
id[idb] = num; //更新根节点的id值
}
}

到这里后,看一下,构造树型id(x)值的具体图:

首先顶点1到5的id(x) = {-1, -1, -1, -1, -1},即表示刚开始,所有顶点均为根节点。(PS:后面示例id(x)、find(x)和union(x, y)中对于数组中元素均为1开始,不是0开始计算数组中元素,这样是方面描述,请大家不要见怪哟。注意,下面图中id = 2表示根节点为顶点3)

(1)选择第1条边,2-3,5

此时id(2) = -1,find(2) = 2根节点为2。id(3) = -1,find(3) = 3根节点为3。根据union(x)函数可知,由于id(find(2)) >= id(find(3)),所以id(find(2)) = idb = 2,id(find(3)) = num = -2

此时id(x) = {-1, 2, -2, -1, -1 }

(2)选择第2条边,1-2,5

此时,id(1) = -1,find(1) = 1根节点为1。Id(2) = 2,find(2) = 3根节点为3。根据union(x)函数可知,由于id(find(1)) > id(find(2)),所以id(find(1)) = idb = 2,id(find(2)) = num = -3

此时id(x) = {2, 2, -3, -1, -1 }

(3)选择第3条边,3-5,6

此时,id(3) = -3,find(3) = 3根节点为3。Id(5) = -1,find(5) = 5根节点为5。根据union(x)函数可知,由于id(find(3))  <  id(find(5)),所以id(find(5)) = ida = 2,id(find(3)) = num = -4

此时id(x) = {2, 2, -4, -1, 2}

(4)选择第4条边,4-5,12(此处也是最小生成树的最后一条边)

此时,id(4) = -1,find(4) = 4根节点为4。Id(5) = 2,find(5) = 3根节点为3。根据union(x)函数可知,由于id(find(4))  >  id(find(5)),所以id(find(4)) = idb = 2,id(find(5)) = num = -5

此时id(x) = {2, 2, -5, 2, 2 }

2.3 具体编码(最佳时间效率)

具体代码如下:

package com.liuzhen.systemExe;

import java.util.ArrayList;
import java.util.Scanner; public class Kruskal {
//内部类,其对象表示连通图中一条边
class edge {
public int a; // 开始顶点
public int b; //结束顶点
public int value; //权值 edge(int a, int b, int value) {
this.a = a;
this.b = b;
this.value = value;
}
}
//使用合并排序,把数组A按照其value值进行从小到大排序
public void edgeSort(edge[] A){
if(A.length > 1) {
edge[] leftA = getHalfEdge(A, 0);
edge[] rightA = getHalfEdge(A, 1);
edgeSort(leftA);
edgeSort(rightA);
mergeEdgeArray(A, leftA, rightA);
}
}
//judge = 0返回数组A的左半边元素,否则返回右半边元素
public edge[] getHalfEdge(edge[] A, int judge) {
edge[] half;
if(judge == 0) {
half = new edge[A.length / 2];
for(int i = 0;i < A.length / 2;i++)
half[i] = A[i];
} else {
half = new edge[A.length - A.length / 2];
for(int i = 0;i < A.length - A.length / 2;i++)
half[i] = A[A.length / 2 + i];
}
return half;
}
//合并leftA和rightA,并按照从小到大顺序排列
public void mergeEdgeArray(edge[] A, edge[] leftA, edge[] rightA) {
int i = 0;
int j = 0;
int len = 0;
while(i < leftA.length && j < rightA.length) {
if(leftA[i].value < rightA[j].value) {
A[len++] = leftA[i++];
} else {
A[len++] = rightA[j++];
}
}
while(i < leftA.length) A[len++] = leftA[i++];
while(j < rightA.length) A[len++] = rightA[j++];
} //获取节点a的根节点编号
public int find(int[] id, int a) {
int i, root, k;
root = a;
while(id[root] >= 0) root = id[root]; //此处,若id[root] >= 0,说明此时的a不是根节点,因为唯有根节点的值小于0
k = a;
while(k != root) { //将a节点所在树的所有节点,都变成root的直接子节点
i = id[k];
id[k] = root;
k = i;
}
return root;
}
//判断顶点a和顶点b的根节点大小,根节点值越小,代表其对应树的节点越多,将节点少的树的节点添加到节点多的树上
public void union(int[] id, int a, int b) {
int ida = find(id, a); //得到顶点a的根节点
int idb = find(id, b); //得到顶点b的根节点
int num = id[ida] + id[idb]; //由于根节点值必定小于0,此处num值必定小于零
if(id[ida] < id[idb]) {
id[idb] = ida; //将顶点b作为顶点a根节点的直接子节点
id[ida] = num; //更新根节点的id值
} else {
id[ida] = idb; //将顶点a作为顶点b根节点的直接子节点
id[idb] = num; //更新根节点的id值
}
}
//获取图A的最小生成树
public ArrayList<edge> getMinSpanTree(int n, edge[] A) {
ArrayList<edge> list = new ArrayList<edge>();
int[] id = new int[n];
for(int i = 0;i < n;i++)
id[i] = -1; //初始化id(x),令所有顶点的id值为-1,即表示为根节点
edgeSort(A); //使用合并排序,对于图中所有边权值进行从小到大排序
int count = 0;
for(int i = 0;i < A.length;i++) {
int a = A[i].a;
int b = A[i].b;
int ida = find(id, a - 1);
int idb = find(id, b - 1);
if(ida != idb) {
list.add(A[i]);
count++;
union(id, a - 1, b - 1);
}
//输出每一次添加完一条边后的所有顶点id值
for(int j = 0;j < id.length;j++)
System.out.print(id[j]+" ");
System.out.println(); if(count >= n - 1)
break;
}
return list;
} public static void main(String[] args){
Kruskal test = new Kruskal();
Scanner in = new Scanner(System.in);
System.out.println("请输入顶点数a和具体边数p:");
int n = in.nextInt();
int p = in.nextInt();
edge[] A = new edge[p];
System.out.println("请依次输入具体边对于的顶点和权值:");
for(int i = 0;i < p;i++) {
int a = in.nextInt();
int b = in.nextInt();
int value = in.nextInt();
A[i] = test.new edge(a, b, value);
}
ArrayList<edge> list = test.getMinSpanTree(n, A);
System.out.println("使用Kruskal算法得到的最小生成树具体边和权值分别为:");
for(int i = 0;i < list.size();i++) {
System.out.println(list.get(i).a+"——>"+list.get(i).b+", "+list.get(i).value);
}
}
}

运行结果:

请输入顶点数a和具体边数p:
5 7
请依次输入具体边对于的顶点和权值:
1 2 5
2 3 5
2 4 12
3 4 17
2 5 15
3 5 6
4 5 12
-1 2 -2 -1 -1
2 2 -3 -1 -1
2 2 -4 -1 2
2 2 -5 2 2
使用Kruskal算法得到的最小生成树具体边和权值分别为:
2——>3, 5
1——>2, 5
3——>5, 6
4——>5, 12

参考资料:

1.《算法设计与分析基础》第3版  (美)Anany Levitin 著  潘彦 译

2.算法训练 安慰奶牛(最小生成树)

算法笔记_066:Kruskal算法详解(Java)的更多相关文章

  1. 【Java学习笔记之三十三】详解Java中try,catch,finally的用法及分析

    这一篇我们将会介绍java中try,catch,finally的用法 以下先给出try,catch用法: try { //需要被检测的异常代码 } catch(Exception e) { //异常处 ...

  2. "二分法"-"折半法"-查找算法-之通俗易懂,图文+代码详解-java编程

    转自http://blog.csdn.net/nzfxx/article/details/51615439 1.特点及概念介绍 下面给大家讲解一下"二分法查找"这个java基础查找 ...

  3. 【Java学习笔记之三十】详解Java单例(Singleton)模式

    概念: Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例.饿汉式单例.登记式单例. 单例模式有以下特点: 1.单例类只能有一个实例. 2.单例类必须自己创建 ...

  4. 算法笔记_071:SPFA算法简单介绍(Java)

    目录 1 问题描述 2 解决方案 2.1 具体编码   1 问题描述 何为spfa(Shortest Path Faster Algorithm)算法? spfa算法功能:给定一个加权连通图,选取一个 ...

  5. 【山外笔记-数据库】Memcached详解教程

    本文打印版文档下载地址 [山外笔记-数据库]Memcached详解教程-打印版.pdf 一.Memcached数据库概述 1.Memcached简介 (1)Memcached是一个自由开源的,高性能, ...

  6. 详解Java GC的工作原理+Minor GC、FullGC

    详解Java GC的工作原理+Minor GC.FullGC 引用地址:http://www.blogjava.net/ldwblog/archive/2013/07/24/401919.html J ...

  7. 第三节:带你详解Java的操作符,控制流程以及数组

    前言 大家好,给大家带来带你详解Java的操作符,控制流程以及数组的概述,希望你们喜欢 操作符 算数操作符 一般的 +,-,*,/,还有两个自增 自减 ,以及一个取模 % 操作符. 这里的操作算法,一 ...

  8. 「跬步千里」详解 Java 内存模型与原子性、可见性、有序性

    文题 "跬步千里" 主要是为了凸显这篇文章的基础性与重要性(狗头),并发编程这块的知识也确实主要围绕着 JMM 和三大性质来展开. 全文脉络如下: 1)为什么要学习并发编程? 2) ...

  9. 算法起步之Kruskal算法

    原文:算法起步之Kruskal算法 说完并查集我们接着再来看这个算法,趁热打铁嘛.什么是最小生成树呢,很形象的一个形容就是铺自来水管道,一个村庄有很多的农舍,其实这个村庄我们可以看成一个图,而农舍就是 ...

随机推荐

  1. jmeter接口测试实践

    一.什么是接口测试? 接口测试是测试系统组件间接口的一种测试.接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点.测试的重点是要检查数据的交换,传递和控制管理过程,以及系统间的相互逻 ...

  2. 如何使用php session

    学会php session可以在很多地方使用,比如做一个后台登录的功能,要让程序记住用户的session,其实很简单,看了下面的文章你就明白了.   PHP session用法其实很简单它可以把用户提 ...

  3. 使用(Drawable)资源——图片资源

    图片资源是最简单的Drawable资源,只要把*.png.*.jpg.*.gif等格式的图片放入/res/drawble-xxx目录下,Android SDK就会在编译应用中自动加载该图片,并在R资源 ...

  4. Canvas rotate- 旋转

    Canvas rotate- 旋转 <!DOCTYPE html> <html lang="en"> <head> <meta chars ...

  5. js如何判断一个变量是否是数组?

    //方法一 var arr = [1,2,3]; var obj = {'name': 'xiaoming','age': 19}; if(arr.constructor == Array){ ale ...

  6. ASP.NET Forms身份认证

    asp.net程序开发,用户根据角色访问对应页面以及功能. 项目结构如下图: 根目录 Web.config 代码: <?xml version="1.0" encoding= ...

  7. 【java设计模式】之 建造者(Builder)模式

    我们还是举上一节的例子:生产汽车.上一节我们通过模板方法模式控制汽车跑起来的动作,那么需求是无止境的,现在如果老板又增加了额外的需求:汽车启动.停止.鸣笛引擎声都由客户自己控制,他想要什么顺序就什么顺 ...

  8. IO的构造方法

    package com.file; /* File的构造方法: File(String pathname); File(File parent,String child); File(String p ...

  9. 二维码 iOS

    一:生成二维码 1.根据一个字符串生成一个二维码  根据 #import <CoreImage/CoreImage.h>这个框架写的 在按钮的点击事件写 @interface ViewCo ...

  10. javascript学习-类型判断

    javascript学习-类型判断 1.类型判断的的武器 javascript中用于类型判断的武器基本上有以下几种: 严格相等===,用来判断null,undefined,true,false这种有限 ...