一,介绍

本文介绍使用Kruskal算法求解无向图的最小生成树。Kruskal是一个贪心算法,并且使用了并查集这种数据结构。
关于并查集的介绍,参考:数据结构--并查集的原理及实现

二,构造一个无向图

图,肯定有顶点和边。由于求解最小生成树,故边还需要有权值。此外,对于每一条边,需要找到与它相关联的两个顶点,因为在将这条边加入到最小生成树时需要判断这两个顶点是否已经连通了。顶点类定义如下:

1     private class Vertex {
2         private String vertexLabel;
3         private List<Edge> adjEdges;// 邻接表
4
5         public Vertex(String vertexLabel) {
6             this.vertexLabel = vertexLabel;
7             adjEdges = new LinkedList<Edge>();
8         }
9     }

表明,图是采用邻接表的形式存储的。

边类的定义如下:

 1 private class Edge implements Comparable<Edge> {
 2         private Vertex startVertex;
 3         private Vertex endVertex;
 4         private int weight;// 边的权值
 5
 6         public Edge(Vertex start, Vertex end, int weight) {
 7             this.startVertex = start;
 8             this.endVertex = end;
 9             this.weight = weight;
10         }
11
12         @Override
13         public int compareTo(Edge e) {
14
15             if (weight > e.weight)
16                 return 1;
17             else if (weight < e.weight)
18                 return -1;
19             else
20                 return 0;
21         }
22     }

边实现了Comparable接口。因为,Kruskal算法使用优先级队列来存储边,边根据权值来进行比较。

假设图存储在一个文件中,每一行包含如下的信息:LinkID,SourceID,DestinationID,Cost(边的编号,起始顶点的标识,终点的标识,边上的权值)

文件格式如下:

无向图如下:

private Map<String, Vertex> nonDirectedGraph;

另外,用一个Map来存储图的顶点,图采用邻接表形式表示。Map的Key为顶点的标识,Value为顶点类。

三,求解最小生成树的Kruskal算法分析

Kruskal算法是一个贪心算法,它与Dijkstra算法非常的相似。Kruskal算法贪心的地方在于:它总是选取图中当前权值最小的边的加入到树中(该边加入到树中之后不能出现环)。因此,这里就有个问题,如何选取当前权值最小的边?

Kruskal算法用到了并查集。因为,算法初始时将图中的各个顶点视为独立的,不连通的,随着一步步将当前权值最小的边加入,就将各个顶点连接起来了(使用并查集的Union操作实现连接)

关于选取最小权值的边,最常用的就是使用优先级队列了,而优先级队列则可以使用二叉堆来实现。关于二叉堆参考:数据结构--堆的实现(下)

算法的总体步骤:

①构造一个无向图啊。求解该图的最小生成树。----需要测试代码是否正确,得有一个实际的图。

②根据无向图中的顶点来 初始化 并查集----初始化过程和 并查集的应用之求解无向图中的连接分量个数 里面讲到的图的初始化过程一样。

③创建一个优先级队列来存储图中的边,这样每次选取边时,直接出队列,这比查找图中所有的边然后选择最小权值的边效率要高一点啊。

④判断这条边关联的两个顶点是否已经连通,如果已经连通了,再将该边加入到生成树中会导致环。

⑤直到所有的顶点都已经并入到生成树时,算法结束。

看完这个过程,感觉这个算法和并查集的应用之求解无向图中的连接分量个数--求解连通分量的算法没啥大区别。

只不过Kruskal算法额外多用了一个优先级队列而已。

四,代码实现

Kruskal算法用到并查集,那肯定需要实现并查集的基本操作,关于并查集,参考:数据结构--并查集的原理及实现

关于并查集基本操作的实现与并查集的应用之求解无向图中的连接分量个数 基本一样。

关于存储并查集的一维数组的说明如下:

使用一个一维数组来存储并查集,这里一维数组的下标表示图的顶点标识,数组元素s[i]有两种表示含义:当数组元素大于0时,表示的是 顶点 i 的父结点位置 ;当数组元素s[i]小于0时,表示的是 顶点 i 为根的子树的高度(秩!)。从而将数组的下标与图的顶点一 一 对应起来。

下面重点来看下最小生成树算法的实现:

 1     /**
 2      *
 3      * @param graph
 4      *            求解graph的一棵最小生成树
 5      * @return 组成最小生成树上的所有的边
 6      */
 7     public List<Edge> kruskal(Map<String, Vertex> graph) {
 8         List<Edge> miniEdges = new ArrayList<Edge>(graph.size() - 1);
 9
10         while (miniEdges.size() != graph.size() - 1) {
11             Edge e = pq.remove();
12             int start = Integer.valueOf(e.startVertex.vertexLabel);
13             int end = Integer.valueOf(e.endVertex.vertexLabel);
14             if (find(start) != find(end)) {
15                 union(start, end);
16                 miniEdges.add(e);
17             }
18         }
19         return miniEdges;
20     }

第8行构造一个ArrayList存储最小生成树中的边。其中,最小生成树中的边的数目为顶点的数目减1。graph.size()返回顶点的个数。

在第10行的while循环中构建最小生成树,第12行和第13行获得顶点的标识。顶点的标识与数组下标对应,比如 顶点4 ’存储‘ 在 s数组中的下标4中。

第14行判断两个顶点是否已经连通,若不连通,则将这条边加入到最小生成树中。初始时,所有的顶点都在各自的子树中,互不连通(见make_set方法)

关于算法效率的一点分析:

上面的方法中,不断地从优先级队列中弹出权值最小的边,在大部分的情况下,是不需要将优先级队列中的所有的边都弹出完的。

但是,存在这样一种情况:图中权值最大的那条边是唯一到某个顶点的路径,则需要把优先级队列中的所有的边都弹出后,才能构造一棵最小生成树。

就拿上面的那个图来说:顶点4到 顶点6 这条边的权值为6,是所有边的最大的,而且到达顶点4只能经过 权值为6 的这条边。而我们所有的边都存储在优先级队列中,故权值为6这条边一定是最后才弹出的,在权值为6的这条边出队列时,图中所有的边都已经出队列了(优先级队列嘛,权值越小,越先出队列)。

假设把顶点4到 顶点6 这条边的权值 6改成 1,在 顶点2 到 顶点5的权值为5的边、顶点3到 顶点6 权值为4 的边 出队列之前,最小生成树就已经构造好了。

因为,优先级队列会优先选择顶点3到顶点2的这条边,以及 顶点5 到 顶点6的边。

五,整个完整代码如下:

  1 import java.util.ArrayList;
  2 import java.util.LinkedHashMap;
  3 import java.util.LinkedList;
  4 import java.util.List;
  5 import java.util.Map;
  6 import java.util.PriorityQueue;
  7
  8 import c9.topo.FileUtil;
  9
 10 public class MinSpanningTree {
 11     private class Vertex {
 12         private String vertexLabel;
 13         private List<Edge> adjEdges;// 邻接表
 14
 15         public Vertex(String vertexLabel) {
 16             this.vertexLabel = vertexLabel;
 17             adjEdges = new LinkedList<Edge>();
 18         }
 19     }
 20
 21     private class Edge implements Comparable<Edge> {
 22         private Vertex startVertex;
 23         private Vertex endVertex;
 24         private int weight;// 边的权值
 25
 26         public Edge(Vertex start, Vertex end, int weight) {
 27             this.startVertex = start;
 28             this.endVertex = end;
 29             this.weight = weight;
 30         }
 31
 32         @Override
 33         public int compareTo(Edge e) {
 34
 35             if (weight > e.weight)
 36                 return 1;
 37             else if (weight < e.weight)
 38                 return -1;
 39             else
 40                 return 0;
 41         }
 42     }
 43
 44     private Map<String, Vertex> nonDirectedGraph;
 45     PriorityQueue<Edge> pq = new PriorityQueue<MinSpanningTree.Edge>();// 优先级队列存储边
 46
 47     private void buildGraph(String graphContent) {
 48         String[] lines = graphContent.split("\n");
 49
 50         String startNodeLabel, endNodeLabel;
 51         Vertex startNode, endNode;
 52         for (int i = 0; i < lines.length; i++) {
 53             String[] nodesInfo = lines[i].split(",");
 54             startNodeLabel = nodesInfo[1];
 55             endNodeLabel = nodesInfo[2];
 56
 57             endNode = nonDirectedGraph.get(endNodeLabel);
 58             if (endNode == null) {
 59                 endNode = new Vertex(endNodeLabel);
 60                 nonDirectedGraph.put(endNodeLabel, endNode);
 61             }
 62
 63             startNode = nonDirectedGraph.get(startNodeLabel);
 64             if (startNode == null) {
 65                 startNode = new Vertex(startNodeLabel);
 66                 nonDirectedGraph.put(startNodeLabel, startNode);
 67             }
 68             Edge e = new Edge(startNode, endNode, Integer.valueOf(nodesInfo[3]));
 69             // 对于无向图而言,起点和终点都要添加边
 70             endNode.adjEdges.add(e);
 71             startNode.adjEdges.add(e);
 72
 73             pq.add(e);// 将边加入到优先级队列中
 74         }
 75     }
 76
 77     private int[] s;// 存储并查集的一维数组
 78
 79     public MinSpanningTree(String graphContent) {
 80         nonDirectedGraph = new LinkedHashMap<String, MinSpanningTree.Vertex>();
 81         buildGraph(graphContent);
 82
 83         make_set(nonDirectedGraph);// 初始化并查集
 84     }
 85
 86     private void make_set(Map<String, Vertex> graph) {
 87         int size = graph.size();
 88         s = new int[size];
 89         for (Vertex v : graph.values()) {
 90             s[Integer.valueOf(v.vertexLabel)] = -1;// 顶点的标识是从0开始连续的数字
 91         }
 92     }
 93
 94     private int find(int root) {
 95         if (s[root] < 0)
 96             return root;
 97         else
 98             return s[root] = find(s[root]);
 99     }
100
101     private void union(int root1, int root2) {
102         if (find(root1) == find(root2))
103             return;
104         // union中的参数是合并任意两个顶点,但是对于并查集,合并的对象是该顶点所在集合的代表顶点(根顶点)
105         root1 = find(root1);// 查找顶点root1所在的子树的根
106         root2 = find(root2);// 查找顶点root2所在的子树的根
107
108         if (s[root2] < s[root1])// root2 is deeper
109             s[root1] = root2;
110         else {
111             if (s[root1] == s[root2])// 一样高
112                 s[root1]--;// 合并得到的新的子树高度增1 (以root1作为新的子树的根)
113             s[root2] = root1;// root1 is deeper
114         }
115     }
116
117     /**
118      *
119      * @param graph
120      *            求解graph的一棵最小生成树
121      * @return 组成最小生成树上的所有的边
122      */
123     public List<Edge> kruskal(Map<String, Vertex> graph) {
124         List<Edge> miniEdges = new ArrayList<Edge>(graph.size() - 1);
125
126         while (miniEdges.size() != graph.size() - 1) {
127             Edge e = pq.remove();
128             //生成并查集操作的对应的标点位置
129             int start = Integer.valueOf(e.startVertex.vertexLabel);
130             int end = Integer.valueOf(e.endVertex.vertexLabel);
131             if (find(start) != find(end)) {
132                 union(start, end);
133                 miniEdges.add(e);
134             }
135         }
136         return miniEdges;
137     }
138
139     // for test purpose
140     public static void main(String[] args) {
141         String graphFilePath;
142         if (args.length == 0)
143             graphFilePath = "F:\\graph.txt";
144         else
145             graphFilePath = args[0];
146
147         String graphContent = FileUtil.read(graphFilePath, null);// 从文件中读取图的数据
148         MinSpanningTree mst = new MinSpanningTree(graphContent);
149
150         // 获得图中组成最小生成树的所有边
151         List<Edge> edges = mst.kruskal(mst.nonDirectedGraph);
152         for (Edge edge : edges) {
153             System.out.print(edge.startVertex.vertexLabel + "-->"
154                     + edge.endVertex.vertexLabel);
155             System.out.println("  weight: " + edge.weight);
156         }
157     }
158 }

FileUtil类可参考中的完整代码实现。

六,实验结果

求得的最小生成树的信息如下:

0-->3 表示顶点0到顶点3的边,边的权值为1

5-->6 表示顶点5到顶点6的边,边的权值为1

.......

分类: 数据结构
本文转自hapjin博客园博客,原文链接:http://www.cnblogs.com/hapjin/p/5490248.html,如需转载请自行联系原作者

并查集与贪心算法的应用之求解无向图的最小生成树相关推荐

  1. 九度OJ 1024 畅通工程 -- 并查集、贪心算法(最小生成树)

    题目地址:http://ac.jobdu.com/problem.php?pid=1024 题目描述: 省政府"畅通工程"的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有 ...

  2. 基于并查集的kruskal算法

    #include <iostream> //并查集的kruskal算法using namespace std;const int max_ve=1005,max_ed=15005;int ...

  3. 亲戚关系关系算法java程序_C++并查集亲戚(Relations)算法实例

    本文实例讲述了C++并查集亲戚(Relations)算法.分享给大家供大家参考.具体分析如下: 题目: 亲戚(Relations) 或许你并不知道,你的某个朋友是你的亲戚.他可能是你的曾祖父的外公的女 ...

  4. 图 相关算法~从头学算法【广搜、 深搜、 拓扑排序、 并查集、 弗洛伊德算法、迪杰斯特拉算法】

    图的相关主流算法主要有: 广度优先搜索 深度优先搜索 拓扑排序 并查集 多源最短路径(弗洛伊德算法) 单源最短路径(迪杰斯特拉算法) 其中呢,最基本的是前两种,也就是平时常用的广搜和深搜,本文中将概要 ...

  5. 关于 并查集(union find) 算法基本原理 以及 其 在分布式图场景的应用

    二月的最后一篇水文-想写一些有意思的东西. 文章目录 环检测在图数据结构中的应用 深度/广度优先 检测环 并查集数据结构 (Union-Find) 基本概念 初始化 合并 union 查找祖先 优化1 ...

  6. 普林斯顿算法(1.3)并查集(union-find算法)——本质就是一个数 下面的子树代表了连在一起的点...

    转自:https://libhappy.com/2016/03/algs-1.3/ 假设在互联网中有两台计算机需要互相通信,那么该怎么确定它们之间是否已经连接起来还是需要架设新的线路连接这两台计算机. ...

  7. jzoj1751-Span(每日C组)【并查集,贪心】

    题目 有n个村,m条路,给n-1条路刷油漆连接n个村,让最长边与最短边的长度差最小. 输入输出(建议跳过) Input 第一行给出一个数字TOT,代表有多少组数据,Tot<=6 对于每组数据,首 ...

  8. 关押罪犯-并查集、贪心

    题目来源:Acwing 257.关押罪犯&洛谷 P1525 [NOIP2010 提高组] 关押罪犯 思路来源:这里 题目描述 S 城现有两座监狱,一共关押着 N 名罪犯,编号分别为1~N. 他 ...

  9. 基于C语言,详解Kruskal算法(利用并查集)实现构建最小生成树

    目录 一.Kruskal算法的基本介绍 具体做法:找出森林中连接任意两棵树的所有边中,具有最小权值的边,如果将它加入生成树中不产生回路,则它就是生成树中的一条边.这里的关键就是如何判断"将它 ...

最新文章

  1. Linux进程的分析和执行过程
  2. python新建文件夹口令_3分钟学会一段Python代码脚本,轻松实现破解FTP密码口令...
  3. bt下载加速 BitTorrent trackers服务器列表
  4. 搭建卷积神经网络怎么确定参数_AI入门:卷积神经网络
  5. 服务提供者框架理解草图
  6. 在.NET外散步之我爱贪吃蛇Python -常见数据结构(新浪和百度云平台即将推出Python免费空间)...
  7. Python检查批量URL是否可以正常访问
  8. 基于D-S证据理论的数据融合算法的研究
  9. 坚持每一天,不忘初心,正经的前端学习(705)
  10. julia集 matlab代码,Julia中文手册1.1版本
  11. MATLAB寻找峰值函数
  12. mysql查询历史时刻数据_跨平台实时数据库查询历史数据的方法介绍
  13. 读书笔记:少有人走的路
  14. 在使用计算机结束时,计算机使用完毕后应将显示器的电源关闭对吗?
  15. js实现刷新当前页面
  16. Netconf配置及其RPC和Notification下发流程解析
  17. 交通预测论文翻译:Deep Learning on Traffic Prediction: Methods,Analysis and Future Directions
  18. texmaker中图片过大怎么办_latex 图或表和正文间距过大怎么处理
  19. 五分之一金融机构将从2018年开始探索加密货币交易
  20. 网上书城项目前端界面设计及编码

热门文章

  1. MariaDB Galera Cluster with HA Proxy and Keepalived on Cents 6
  2. memcached 安装小结-1
  3. Navicat 10 for SQL Server - 绿色中文版
  4. Nginx下完美解决WordPress的伪静态 (wordpress 迁移后 导致 页面404)
  5. U启动安装原版Win7系统教程
  6. python的简介和入门
  7. Javascript图像处理之将彩色图转换成灰度图
  8. Handbook of Constraints Programming——Chapter 22 Constraint-Based Scheduling and Planning
  9. linux 的读写操作(转)
  10. .NET Framework 4.0 和 Dublin 中的 WCF 和 WF 服务 - z