数据结构与算法--最小生成树之Kruskal算法
数据结构与算法--最小生成树之Kruskal算法
上一节介绍了Prim算法,接着来看Kruskal算法。
我们知道Prim算法是从某个顶点开始,从现有树周围的所有邻边中选出权值最小的那条加入到MST中。不妨换个思路,为何不一开始就将所有边中权值最小的边取出来搭建二叉树?这里说的最小权值是全局的最小权值,而Prim说的最小权值,是已经访问过的顶点的周围的边中的最小权值,这个范围当然比全部边要小。
于是需要对边按照权值升序排列,由于每次取出的最小权值分布在图的各个地方,一开始各条边可能并不是相连的,图中就会形成多棵树。随着添加的边越来越多,两棵树合并成一棵树(用可以找到的权值最小的边连接),如此合并下去,最后只剩下一棵,这就是最小生成树。选出的边不能造成一棵树产生环,而树可以看作连通分量,不能有环就是说——处于同一个连通分量中的任意两个结点不能再将它们连通(否则必然成环)。所以我们在选出边准备连接树的时候一定要判断,选出的边如果其两个顶点处于同一个连通分量中,那么要跳过本次连接。
关键就是如何判断两个顶点是否处于同一连通分量;以及如果两个顶点不处于同一连通分量,如何将他们合并到同一连通分量中?
为此,我们要先实现一个UnionFind类来完成上述工作。
连通分量--UnionFind类的实现
package Chap7;
public class UnionFind {
// id相同的分量是连通的
private int[] id;
//连通分量的个数
private int count;
public UnionFind(int num) {
count = num;
id = new int[num];
for (int i = 0; i < num; i++) {
id[i] = i;
}
}
public int count() {
return count;
}
// 所属连通分量的id
public int find(int p) {
return id[p];
}
public void union(int p ,int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID) {
return;
}
// 将和p同一个连通分量的结点全部归到和q一个分量中,即将p所在连通分量与q所在连通分量合并。
// 反过来也可以
// if (id[i] == qID) {
// id[i] = pID;
// }
for (int i = 0; i < id.length; i++) {
if (id[i] == pID) {
id[i] = qID;
}
}
// 合并后,连通分量减少1
count--;
}
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
}
至关重要的就是这个id[]标识数组了。它的功能就是将处于同一连通分量的结点归并到同一个id中,也就是说如果某两个结点他们的id相同,那么他们就处于同一个连通分量中。比如id[3] = id[4]= id[6] = 8,id[2] = id[4] = id[9] = 1,那么结点3,4,6处于同一个连通分量中,结点2、4、9处于另外一个连通分量中。
find就是找到某结点所属的连通分量,如果find到两个结点属于同一个连通分量(id),isConnected返回true。
union方法将两个不连通的分量连通。具体做法是,若结点p和q不属于同一个连通分量,则将p所在的连通通分量A的全部结点的id改成和q所在连通分量B的id一样——也就是连接p-q,连通分量A和连通分量B就将合并成一个连通分量了。
这个算法很简单,不过有待优化,可以看到每次合并分量都遍历了整个id[],所以针对id[]进行优化。优化后id[]更像是一种树形结构,最早存在于连通分量中的为该连通分量的根结点,之后加进来的结点成为根结点的孩子结点。因为一棵树就是一个连通分量,如果两个结点往上追溯,他们有相同的根结点,说明这两个结点位于同一棵树中,也就是说这两个结点位于同一个连通分量中。
基于此思想,我们来修改下UnionFind的代码,只需变动find和union方法。
package Chap7;
public class UnionFind {
//
private int[] parentTo;
//连通分量的个数
private int count;
public UnionFind(int num) {
count = num;
parentTo = new int[num];
for (int i = 0; i < num; i++) {
parentTo[i] = i;
}
}
public int count() {
return count;
}
public int find(int p) {
// p = parentTo[p]说明到达树的根结点,返回根结点
while (p != parentTo[p]) {
p = parentTo[p];
}
return p;
}
public void union(int p,int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
// 这行的意思就是q所在连通分量和q所在连通分量合并
// 从树的角度来看,p树的根结点成为了q树根结点的孩子结点
// 反过来也可以,parentTo[qRoot] = pRoot;
parentTo[pRoot] = qRoot;
count--;
}
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
}
首先我们将id[]改成了parentTo[],命名更易于理解。未优化的版本id[]存放的是各个结点所属的连通分量的标识,id表达的意义再合适不过。不过在优化的版本中,该数组是树形结构了,里面存的是某结点的父结点,用parentTo比较恰当,比如parent[3] = 4表达的意思是结点3的父结点是4。
上图的有两个连通分量,其中1和8作为根结点,可以看到根结点有p = parent[p]这一特点。它们都是初始值,从初始化代码中可以看出一开始parent[i] = i,所谓的根结点其实就是parent[i]从来没有被赋值过。
现在find(int p)方法就比较好理解了,它找到的就是p所在连通分量(树)的根结点。如果两个结点它们的根结点相同,说明处于同一个连通分量(同一棵树),这就解释了isConnected方法的实现。再看union方法,如果两个结点属于不同的树,将这两棵树连接,p树的根结点成为了q树根结点的孩子结点,其实就是两个连通分量合并成一个连通分量。和未优化版本相比,合并分量只修改了数组中一个元素的值,不用遍历整个数组了。
下图给出了分量合并的过程。
Kruskal算法实现
好了,有了这个工具,实现Kruskal算法就相当简单了。
package Chap7;
import java.util.*;
public class Kruskal {
private Queue<Edge> mst;
public Kruskal(EdgeWeightedGraph<?> graph) {
mst = new LinkedList<>();
Queue<Edge> edges = new PriorityQueue<>(graph.edges());
UnionFind uf = new UnionFind(graph.vertexNum());
// 只要mst边小于n-1就进入循环,在循环内添加一条后就变成了n-1条
while (!edges.isEmpty() && mst.size() < graph.vertexNum() -1) {
// 优先队列,删除最小权值的边
Edge edge = edges.poll();
int v = edge.either();
int w = edge.other(v);
if (uf.isConnected(v, w)) {
continue;
}
uf.union(v, w);
mst.offer(edge);
}
}
public Iterable<Edge> edges() {
return mst;
}
public double weight() {
return mst.stream().mapToDouble(Edge::weight).sum();
}
public static void main(String[] args) {
List<String> vertexInfo = Arrays.asList("v0", "v1", "v2", "v3", "v4", " v5", "v6", "v7");
int[][] edges = {{4, 5}, {4, 7}, {5, 7}, {0, 7},
{1, 5}, {0, 4}, {2, 3}, {1, 7}, {0, 2}, {1, 2},
{1, 3}, {2, 7}, {6, 2}, {3, 6}, {6, 0}, {6, 4}};
double[] weight = {0.35, 0.37, 0.28, 0.16, 0.32, 0.38, 0.17, 0.19,
0.26, 0.36, 0.29, 0.34, 0.40, 0.52, 0.58, 0.93};
EdgeWeightedGraph<String> graph = new EdgeWeightedGraph<>(vertexInfo, edges, weight);
Kruskal kruskal = new Kruskal(graph);
System.out.println("MST的所有边为:" + kruskal.edges());
System.out.println("最小成本和为:" + kruskal.weight());
}
}
代码流程比Prim算法清晰得多,理解起来也容易。无非是取出一条最小权值的边,先判断这条边中的两个顶点是否位于同一个连通分量中(同一棵树),若已连通则忽略这条边;否则将这两个顶点连通,并将这条边加入到MST。
最后注意一点,我们的Prim算法和Kruskal算法都是针对无向加权图,若是要对有向加权图处理,那被称为最小树形图,所用到的算法是朱刘算法。
by @sunhaiyu
2017.9.21
数据结构与算法--最小生成树之Kruskal算法的更多相关文章
- 最小生成树的Kruskal算法实现
最近在复习数据结构,所以想起了之前做的一个最小生成树算法.用Kruskal算法实现的,结合堆排序可以复习回顾数据结构.现在写出来与大家分享. 最小生成树算法思想:书上说的是在一给定的无向图G = (V ...
- 数据结构与算法--最小生成树之Prim算法
数据结构与算法--最小生成树之Prim算法 加权图是一种为每条边关联一个权值或称为成本的图模型.所谓生成树,是某图的一棵含有全部n个顶点的无环连通子图,它有n - 1条边.最小生成树(MST)是加权图 ...
- 【转载】最小生成树之Kruskal算法
给定一个无向图,如果它任意两个顶点都联通并且是一棵树,那么我们就称之为生成树(Spanning Tree).如果是带权值的无向图,那么权值之和最小的生成树,我们就称之为最小生成树(MST, Minim ...
- 算法起步之Kruskal算法
原文:算法起步之Kruskal算法 说完并查集我们接着再来看这个算法,趁热打铁嘛.什么是最小生成树呢,很形象的一个形容就是铺自来水管道,一个村庄有很多的农舍,其实这个村庄我们可以看成一个图,而农舍就是 ...
- 算法笔记_066:Kruskal算法详解(Java)
目录 1 问题描述 2 解决方案 2.1 构造最小生成树示例 2.2 伪码及时间效率分析 2.3 具体编码(最佳时间效率) 1 问题描述 何为Kruskal算法? 该算法功能:求取加权连通图的最小 ...
- [数据结构]最小生成树算法Prim和Kruskal算法
最小生成树 在含有n个顶点的连通图中选择n-1条边,构成一棵极小连通子图,并使该连通子图中n-1条边上权值之和达到最小,则称其为连通网的最小生成树. 例如,对于如上图G4所示的连通网可以有多棵权值总 ...
- 无向带权图的最小生成树算法——Prim及Kruskal算法思路
边赋以权值的图称为网或带权图,带权图的生成树也是带权的,生成树T各边的权值总和称为该树的权. 最小生成树(MST):权值最小的生成树. 生成树和最小生成树的应用:要连通n个城市需要n-1条边线路.可以 ...
- 邻接矩阵c源码(构造邻接矩阵,深度优先遍历,广度优先遍历,最小生成树prim,kruskal算法)
matrix.c #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include < ...
- HDU1875——畅通工程再续(最小生成树:Kruskal算法)
畅通工程再续 Description相信大家都听说一个“百岛湖”的地方吧,百岛湖的居民生活在不同的小岛中,当他们想去其他的小岛时都要通过划小船来实现.现在政府决定大力发展百岛湖,发展首先要解决的问题当 ...
随机推荐
- 【推荐】介绍两款Windows资源管理器,Q-Dir 与 FreeCommander XE(比TotalCommander更易用的免费资源管理器)
你是否也像我一样,随着硬盘.文件数量的增加,而感到对于文件的管理越来越乏力. 于是我试用了传说中的各种软件,包括各种Explorer外壳,或者第三方资源管理器. 最后我确定下来经常使用,并推荐给您的是 ...
- 静态工厂 + DbHelper
在 .NET 的学习过程中用过三种数据库:Sql Server.Access.SQLite.Sql Server 用得相对多一点点,但是,麻烦,每次用它都需要开服务,而且还费资源,更麻烦的是拷贝到一台 ...
- WPF Bitmap转成Imagesource的性能优化
之前有个需求是在WPF中生成二维码,用的是QRCoder. QRCoder生成的是Bitmap,在wpf中需要转换成ImageSource才能显示. 之前的转换方式是: IntPtr hBitmap ...
- 使用WPF制作视频监控多画面切换
前言 曾有做过一个产品,有一个功能是视频监控模块,视频监控首先想到的是视频多画面切换功能,由于前端是用WPF开发的,所以当时就做了一个多画面切换组件,效果如下: 功能设计前提: 由于要使用海康大华天地 ...
- 企业项目开发--本地缓存guava cache(2)
此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. AdminCacheKey: package com.xxx.vo.userManagement; /** ...
- 201621123018《Java程序设计》第6周学习报告
1. 本周学习总结 1.1 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图或相关笔记,对面向对象思想进行一个总结. 2. 书面作业 1. clone方法 1.1 在 ...
- spring cloud学习(五) 配置中心
Spring Cloud Config为服务端和客户端提供了分布式系统的外部化配置支持.配置服务中心采用Git的方式存储配置文件,因此我们很容易部署修改,有助于对环境配置进行版本管理. 一.配置中心 ...
- mongoose 基础api 图表整理
一.背景 今天看 mongoose 的基础 API,参考了下面的链接做了图表以供查阅. 参考资料: http://www.cnblogs.com/xiaohuochai/p/7215067.html ...
- postgresql-int,bigint,numeric效率测试
在postgresql9.5的时候做过一个测试就是sum()的效率最终的测试结果是sum(int)>sum(numeric)>sum(bigint)当时比较诧异为啥sum(bigint)效 ...
- 09-01 Java final,多态,抽象类,接口
final /* final可以修饰类,方法,变量 特点: final可以修饰类,该类不能被继承. final可以修饰方法,该方法不能被重写.(覆盖,复写) final可以修饰变量,该变量不能被重新赋 ...