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)中要求的功能。

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

id(x)生成一个单元集合{x}。假设这个操作对集合S的每一个元素只能应用一次。

find(x)返回一个包含x的子集。

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

java实现Kruskal算法的更多相关文章

  1. Kruskal算法java版

    /** * sample Kruskal.java Description: * kruskal算法的思想是找最小边,且每次找到的边不会和以找出来的边形成环路,利用一个一维数组group存放当前顶点所 ...

  2. Kruskal算法(三)之 Java详解

    前面分别通过C和C++实现了克鲁斯卡尔,本文介绍克鲁斯卡尔的Java实现. 目录 1. 最小生成树 2. 克鲁斯卡尔算法介绍 3. 克鲁斯卡尔算法图解 4. 克鲁斯卡尔算法分析 5. 克鲁斯卡尔算法的 ...

  3. 算法笔记_066:Kruskal算法详解(Java)

    目录 1 问题描述 2 解决方案 2.1 构造最小生成树示例 2.2 伪码及时间效率分析 2.3 具体编码(最佳时间效率)   1 问题描述 何为Kruskal算法? 该算法功能:求取加权连通图的最小 ...

  4. java实现最小生成树的prim算法和kruskal算法

    在边赋权图中,权值总和最小的生成树称为最小生成树.构造最小生成树有两种算法,分别是prim算法和kruskal算法.在边赋权图中,如下图所示: 在上述赋权图中,可以看到图的顶点编号和顶点之间邻接边的权 ...

  5. 最小生成树---Prim算法和Kruskal算法

    Prim算法 1.概览 普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (gra ...

  6. Kruskal算法(一)之 C语言详解

    本章介绍克鲁斯卡尔算法.和以往一样,本文会先对克鲁斯卡尔算法的理论论知识进行介绍,然后给出C语言的实现.后续再分别给出C++和Java版本的实现. 目录 1. 最小生成树 2. 克鲁斯卡尔算法介绍 3 ...

  7. 算法起步之Kruskal算法

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

  8. 算法8-4:Kruskal算法

    Kruskal算法用于计算一个图的最小生成树.这个算法的过程例如以下: 依照边的权重从小到达进行排序 依次将每条边添加到最小生成树中,除非这条边会造成回路 实现思路 第一个步骤须要对边进行排序,排序方 ...

  9. 【算法设计与分析基础】24、kruskal算法详解

    首先我们获取这个图 根据这个图我们可以得到对应的二维矩阵图数据 根据kruskal算法的思想,首先提取所有的边,然后把所有的边进行排序 思路就是把这些边按照从小到大的顺序组装,至于如何组装 这里用到并 ...

随机推荐

  1. [hdu5348]图上找环,删环

    http://acm.hdu.edu.cn/showproblem.php?pid=5348 题意:给一个无向图,现在要将其变成有向图,使得每一个顶点的|出度-入度|<=1 思路:分为两步,(1 ...

  2. [hdoj5192] 树状数组

    枚举所有的区间.对于确定的区间,假设最终的高度为h, 代价是max(∑(Hi−h),∑(h−Hj))(Hi>h,Hj≤h) 等价于max(∑Hi−cnt(i)∗h,cnt(j)∗h−∑Hj) ( ...

  3. 关于fromdata的上传文件问题

    <div <label>上传pdf</label> <input id="fileId" type="file" accep ...

  4. 可能是把 Java 接口讲得最通俗的一篇文章

    读者春夏秋冬在抽象类的那篇文章中留言,"二哥,面试官最喜欢问的一个问题就是,'兄弟,说说抽象类和接口之间的区别?',啥时候讲讲接口呗!" 对于面向对象编程来说,抽象是一个极具魅力的 ...

  5. Java 在Excel中创建透视表

    本文内容介绍通过Java程序在Excel表格中根据数据来创建透视表. 环境准备 需要使用Excel类库工具—Free Spire.XLS for Java,这里使用的是免费版,可通过官网下载Jar包并 ...

  6. 解决 es CircuitBreakingException 问题

    比如频繁报如下错误, [2019-06-16T15:31:22,778][DEBUG][o.e.a.a.c.n.i.TransportNodesInfoAction] [node-xxx] faile ...

  7. 汉语拼音转换工具包pypinyin

    #pip install pypinyin汉字转换汉语拼音 from pypinyin import lazy_pinyin,TONE,TONE2,TONE3 str="你知道我是谁吗?&q ...

  8. 201771010128王玉兰《面向对象程序设计(Java)》第十三周学习总结

    第一部分:基础理论知识 1.事件处理基础 事件源(event source):能够产生事件的对象都可 以成为事件源,如文本框.按钮等.一个事件源是一个 能够注册监听器并向监听器发送事件对象的对象. 事 ...

  9. Null passed to a callee that requires a non-null argument

    OC中定义的方法参数默认是不为空的,如果能够为空需要手动指定__nullable ,我想这个警告是提示开发者警惕可能空参数

  10. 四、$jQuery

    1.你觉得jQuery或zepto源码有哪些写的好的地方 jquery源码封装在一个匿名函数的自执行环境中,有助于防止变量的全局污染,然后通过传入window对象参数,可以使window对象作为局部变 ...