这里的图指的是带权无向图,也就是无向网。
关于最小生成树

图的最小生成树要解决的问题:用最小的代价连通图中的所有顶点。

下面两种算法都是运用贪心思想,利用MST(Minimum Spanning Tree)性质构建最小生成树。

MST性质: 假设N=(V, E)是一个连通网,U是顶点集V的一个非空子集。若(u, v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u, v)的最小生成树。

证明: 假设网N的任何一棵最小生成树都不包含(u, v)。设T是连通网上的一棵最小生成树,当边(u, v)加入到T中时,由生成树的定义,T中必存在一条包含(u, v)的回路。在这个回路中任意去除一条除(u, v)的边便可消除回路,同时得到另一棵生成树。因为新包含(u, v)的生成树代价小于T,由此和假设矛盾。

1.普里姆算法(Prim)

普里姆算法主要针对的是邻接矩阵形式存储的稠密网。
普里姆算法视频讲解

算法描述

假设N=(V, E)是连通网,TE是N上最小生成树中边的集合,U是N上最小生成树的顶点集合。算法从U={uou_ouo​} (uou_ouo​∈V),TE={ }开始,重复执行下述操作:在所有u∈U,v∈V-U的边(u, v)∈E中找一条代价最小的边(u0,v0u_0,v_0u0​,v0​)并入集合TE,同时v0v_0v0​并入U,直至U=V为止。此时TE中必有n-1条边,则T=(U, TE)为N的最小生成树。

普里姆算法的具体实现

在实际的算法实现中,需要创建两个辅助数组 closest 和 lowcost。

lowcost 数组记录各顶点到在建生成树的最小权值,例如lowcost[ i ]=0代表顶点viv_ivi​已经被加入到在建生成树的顶点集U中,lowcost[ i ]=+∞+\infty+∞表示顶点viv_ivi​不在U中且与在建生成树中的任意顶点均不直接连通,lowcost[ i ]=num表示顶点viv_ivi​不在U中且与在建生成树中的某些顶点直接连通并且所有连通的边中权值最小的边权值为num。

closest 数组记录顶点viv_ivi​到生成树的最小权值边在生成树一端的顶点。

lowcost 数组首先需要根据邻接矩阵初始化为起始顶点viv_ivi​到其它各顶点的权值,lowcost[ j ] = matrix[ locate(viv_ivi​) ][ j ],因为无向网中matrix[ locate(viv_ivi​) ][ locate(viv_ivi​) ]为+∞+\infty+∞,所以根据lowcost数组的定义还需要将lowcost[ locate(viv_ivi​) ] 初始化为0。closest数组初始化为起始顶点viv_ivi​。

以上面左图无向网的邻接矩阵为例,普里姆算法的java实现:

public class Prim {char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F' };                // 顶点数组int[][] matrix = new int[6][6];                                 // 邻接矩阵int INF = 1 << 31 - 1;                                            // INF表示正无穷// 创建邻接矩阵private void creatMartix() {matrix[locate('A')][locate('B')] = matrix[locate('B')][locate('A')] = 6;matrix[locate('B')][locate('E')] = matrix[locate('E')][locate('B')] = 3;matrix[locate('E')][locate('F')] = matrix[locate('F')][locate('E')] = 6;matrix[locate('F')][locate('D')] = matrix[locate('D')][locate('F')] = 2;matrix[locate('D')][locate('A')] = matrix[locate('A')][locate('D')] = 5;matrix[locate('C')][locate('A')] = matrix[locate('A')][locate('C')] = 1;matrix[locate('C')][locate('B')] = matrix[locate('B')][locate('C')] = 5;matrix[locate('C')][locate('E')] = matrix[locate('E')][locate('C')] = 6;matrix[locate('C')][locate('F')] = matrix[locate('F')][locate('C')] = 4;matrix[locate('C')][locate('D')] = matrix[locate('D')][locate('C')] = 5;for (int i = 0; i < matrix.length; i++) {for (int j = 0; j < matrix.length; j++) {if (matrix[i][j] == 0) {matrix[i][j] = INF;}}}}private int locate(char v) {int i = 0;for (; i < vertex.length; i++) {if (v == vertex[i])break;}return i;}// 普里姆算法private void prim(char v) {int[] lowcost = new int[matrix.length];char[] closest = new char[matrix.length];// lowcost、closest数组的初始化for (int j = 0; j < matrix.length; j++) {lowcost[j] = matrix[locate(v)][j];closest[j] = v;}lowcost[locate(v)] = 0;int mincost;int k = 0;                                                   // 保存离在建生成树最近顶点的数组下标for (int i = 1; i < matrix.length; i++) {                     // n个顶点的图需要寻找n-1次mincost = INF;// 根据各顶点到在建生成树的最小权值找出离在建生成树最近的顶点for (int j = 0; j < matrix.length; j++) {// lowcost[j]!=0限制只在所有生成树外的顶点与生成树之间的边中寻找if (lowcost[j] != 0 && lowcost[j] < mincost) {mincost = lowcost[j];k = j;}}// 根据找到的最近顶点输出最短边的信息System.out.println("边 (" + closest[k] + "," + vertex[k] + ") 权:" + mincost);// 将这个顶点加入到生成树中lowcost[k] = 0;// 因为生成树中新加入了顶点,所以需要重新更新在建生成树以外的顶点到在建生成树的最小距离for (int j = 0; j < matrix.length; j++) {if (lowcost[j] != 0 && matrix[k][j] < lowcost[j]) {lowcost[j] = matrix[k][j];closest[j] = vertex[k];}}}}public static void main(String[] args) {Prim p = new Prim();p.creatMartix();p.prim('A');}
}

算法分析

  • 普里姆算法是通过逐渐增加在建生成树中的顶点完成生成树的构建,所以也叫加点法。
  • 因为代码中的双层for循环,所以时间复杂度O(n2)Ο(n^2)O(n2),nnn为图的顶点个数。
  • 空间复杂度等于两个辅助数组的空间,为O(n)Ο(n)O(n)。
  • 从时间复杂度可以看出程序求解的时间与顶点总数有关,与边的数量无关,因此适用于求稠密网的最小生成树。
  • 当选最小权值边时如果存在多条权值相等的边,可以任选一个。因此当存在多条权值相等的边时,该算法构建的生成树可能不唯一,但总权值一定相等。

2.克鲁斯卡尔算法(Kruskal)

假设连通网N=(V, E),则令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V, { }),图中每个顶点自成一个连通分量。在E中选择代价最小的边,若该边依附的顶点落在T不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边。依次类推,直至T中所有顶点都在同一连通分量上为止。

算法的关键点在于如何判断代价最小的边加入T后是否落在T的不同连通分量上,也就是判断T中是否出现了环,这里使用了一种叫做并查集的数据结构,通过这种数据结构可以方便的查找一个元素所属的集合和对两个集合进行合并。并查集(disjoint set)视频讲解。

这种数据结构的实现是:

  • T中不同的连通分量作为不同子树,根据树的双亲表示法将这些子树存储,同一棵子树代表同一个连通分量的顶点集合,判断两个顶点是否属于一个集合只需要判断至两个顶点所在子树的根结点是否相同。
  • 如果边的两个顶点属于一个集合,说明这两个顶点之间一定连通,此时如果将这条边加入则这两个顶点之间存在两条路径,构成环。反之如果不属于一个集合,则将该边加入不会构成环。
  • 该边加入后将两个没有联系的集合联系了起来,所以加入一条边之后需要将该边两个顶点所在的子树合并,合并时为了避免容易增加树的高度,增大之后的查询所属集合耗费的时间,可以选择将高度较小的子树的根结点接在高度较大子树的根结点上。

用克鲁斯卡尔算法生成上面左图无向网的最小生成树:

public class Kruskal {class Edge {char begin;char end;int weight;public Edge(char begin, char end, int weight) {this.begin = begin;this.end = end;this.weight = weight;}}char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F' };Edge[] edges;/** 这里为了方便直接按权值大小顺序手动添加网的所有边。* 也可以根据邻接矩阵的下三角或者上三角生成边数组,* 生成之后还要根据边的权值大小对生成的边数组排序。*/private void creatEdges() {edges = new Edge[10];edges[0] = new Edge('A', 'C', 1);edges[1] = new Edge('D', 'F', 2);edges[2] = new Edge('B', 'E', 3);edges[3] = new Edge('C', 'F', 4);edges[4] = new Edge('B', 'C', 5);edges[5] = new Edge('C', 'D', 5);edges[6] = new Edge('A', 'D', 5);edges[7] = new Edge('A', 'B', 6);edges[8] = new Edge('C', 'E', 6);edges[9] = new Edge('E', 'F', 6);}class TreeNode {char verName;                                     // 结点表示的顶点int parent;                                           // 结点父结点的位置下标int rank;                                          // 保存以该结点为根结点的树的高度public TreeNode(char verName) {this.verName = verName;}}TreeNode[] tree = { new TreeNode('A'), new TreeNode('B'), new TreeNode('C'), new TreeNode('D'), new TreeNode('E'), new TreeNode('F') };private void initTree() {for (int i = 0; i < tree.length; i++) {tree[i].parent = i;                               // 初始每个结点的父结点都指向自己tree[i].rank = 1;                                // 所有单独的顶点都是一棵高度为1的树}}private void kruskal() {creatEdges();initTree();int i = 1;int j = 0;char begin;char end;// n个顶点的最小生成树n-1条边,所以需要循环添加n-1次while (i <= vertex.length - 1) {begin = edges[j].begin;end = edges[j].end;// 如果边的两个顶点属于两棵不同的子树则加入该边不会构成环if (findRoot(begin) != findRoot(end)) {// 输出边的信息并根据边两端的顶点将顶点所在子树合并System.out.println("边 (" + begin + "," + end + ") 权:" + edges[j].weight);union(begin, end);i++;}// 判断下一条边j++;}}// 将两棵不相交的子树合并private void union(char begin, char end) {int i = findRoot(begin);int j = findRoot(end);// begin顶点所在子树的高度大于end顶点所在子树的高度if (tree[i].rank > tree[j].rank) {// end顶点所在子树的根结点连接在begin顶点所在子树的根结点tree[j].parent = i;} else {// 否则begin顶点所在子树的根结点连接在end顶点所在子树的根结点tree[i].parent = j;// 如果两棵子树高度相等,那么连接之后需要将end顶点所在子树的高度+1if (tree[i].rank == tree[j].rank) {tree[j].rank++;}}}// 根据所给顶点返回顶点所在的子树的根结点位置private int findRoot(char c) {// 找到所给顶点的位置int index = locate(c);// 根据所给顶点循环找到所在子树的根结点,根结点的特点是父结点也指向自己while (index != tree[index].parent) {index = tree[index].parent;}return index;}// 返回顶点在所有子树的存储数组中的位置private int locate(char v) {int i = 0;for (; i < tree.length; i++) {if (v == tree[i].verName)break;}return i;}public static void main(String[] args) {Kruskal k = new Kruskal();k.kruskal();}
}

算法分析

  • 克鲁斯卡尔算法是通过逐渐为非连通图添加边数来构建生成树,所以也叫加边法。
  • 算法执行的时间主要包括对边进行排序的时间和并查集查找根结点的时间。设e条边,用堆排序对边进行排序的时间为O(elog2e)Ο(elog_2e)O(elog2​e)。选边操作最多执行e次,因为并查集构建的树高度一定小于边数,所以每次查找根结点的时间都小于O(log2e)Ο(log_2e)O(log2​e),所以总的时间复杂度O(elog2e)Ο(elog_2e)O(elog2​e)。
  • 算法的执行时间只与边数e有关,与顶点总数无关,因此适用于求稀疏网的最小生成树。
  • 同样当有多条权值相等的边且加入非连通图后都落在两个不同的连通分量上不构成环,可以任选一条边加入。所以该算法得到的生成树可能不唯一,但总代价都相等。

【数据结构】——图的最小生成树算法(普里姆+克鲁斯卡尔)相关推荐

  1. 最小生成树算法——普里姆算法

    普里姆算法 假设G=(V,E)是一个具有n个顶点的连通网,T=(U,TE)是G的最小生成树,其中U是T的顶点集,TE是T的边集,U和TE的初始值为空.算法过程: 首先从V中任取一个顶点(假定v1),将 ...

  2. 普里姆 克鲁斯卡尔算法

    一.简介 连通图:任意2节点之间都有路径相通 最小生成树:最小权重生成树 一个 n 结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边(n-1). 最 ...

  3. 数据结构(五)图---最小生成树(普里姆算法)

    一:最小生成树 (一)定义 我们把构造连通网的最小代价生成树称为最小生成树或给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树. (二)什么是最小生成树? 1. ...

  4. 用c语言描述普里姆算法和克鲁斯卡尔算法,克鲁斯卡尔算法+普里姆算法 详解

    克鲁斯卡尔算法: [1]克鲁斯卡尔算法 普里姆算法是以某顶点为起点,逐步找各顶点上最小权值的边来构建最小生成树. 克鲁斯卡尔算法是直接以边为目标去构建. 因为权值是在边上,直接去找最小权值的边来构建生 ...

  5. 数据结构与算法(7-3)最小生成树(普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法)

    目录 一.最小生成树简介 二.普里姆算法(Prim) 1.原理 2.存储 2-1.图顶点和权: 2-3. 最小生成树: 3.Prim()函数 3-1.新顶点入树 3-2.保留最小权 3-3. 找到最小 ...

  6. 最小生成树之普里姆算法

    为了能够讲明白这个算法,我们先构造网图的邻接矩阵,如图7-6-3的右图所示. 也就是说,现在我们已经有了一个存储结构为MGraph的MG(见<邻接矩阵创建图>).MG有9个顶点,它的二维数 ...

  7. 最小生成树(克鲁斯卡尔算法 普里姆算法)

    最小生成树是处理图结构中,简化图的算法:即删除一些边使得图得以简化,但应保证图中任意点都是相连通的.形成的最小生成树应该使得从顶点遍历时走过边的权值和最小.(有n个节点,则最小生成树的边数应为n-1) ...

  8. 最小生成树(普里姆算法)

    关于什么是Prim(普里姆算法)? 在实际生活中,我们常常碰到类似这种一类问题:如果要在n个城市之间建立通信联络网, 则连通n个城市仅仅须要n-1条线路.这时.我们须要考虑这样一个问题.怎样在最节省经 ...

  9. 最小生成树:克鲁斯卡尔算法+普里姆算法

    目录 一.最小生成树 二.克鲁斯卡尔算法 1.思路 2.示例 3.C语言代码 三.普里姆算法 1.思路 2.C语言代码 一.最小生成树 一棵最小生成树需要满足哪些条件呢? 不存在回路 对于具有n个顶点 ...

最新文章

  1. SAP RETAIL 特征参数文件(Characteristic Profile) II
  2. 打开数“智”化之门,一字之差带来的思考
  3. 如何设置listview每个item高度
  4. TensorFlow随笔-多分类单层神经网络softmax
  5. 如何创建最简单的 ABAP 数据库表,以及编码从数据库表中读取数据 (上)
  6. (运维日志)在win7安装Oracle并部署Oracle数据库
  7. 个税倒推收入的计算器_手把手教你做个税计算器(1)
  8. 11计算机专业vb试题答案,西华师范大学计算机VB试题及答案11
  9. python实现自动开机_python自动循环定时开关机(非重启)测试
  10. 风吹雨PHP多应用授权系统【开源】
  11. html+css+javascript+jquery+bootstarp响应式旅行社旅游平台网站模板(14页)
  12. 人生顿悟之博观而约取,厚积而薄发
  13. 亲属关系--并查集训练T1
  14. 计算机网络信息安全参考文献,计算机网络信息安全学论文参考文献 计算机网络信息安全专著类参考文献有哪些...
  15. 在线epub转txt格式如何转换
  16. ocelot和nginx比较_针对 Ocelot 网关的性能测试
  17. macd 公式 java_EMA指标和MACD指标的JAVA语言实现
  18. oracle中毒,oracle数据库中毒恢复 oracle数据库解密恢复 服务器中勒索病毒解密恢复.Hermes666...
  19. linux 磁盘io技术3------libaio使用介绍
  20. 【OFDM系列8】对知乎“正交频分复用(OFDM)原理灵魂9问”的理解与通俗易懂回答(慎入,含大量重要公式详细推导的万字长文)

热门文章

  1. cmd搭建vue前端项目详细过程
  2. SAAS应用灰度设计
  3. Android语音播报、后台播报、语音识别,android程序开发
  4. Spark开发之maven配置
  5. UGUI学习笔记(十二)自制血条控件
  6. C语言:输入1到5的阶乘
  7. zabbix的简单介绍
  8. One Piece 海贼王动漫壁纸 Python 爬取!
  9. 【头歌】 标准ACL配置
  10. linux把目录下的文件设置属性为rx,Linux常用命令(六)管理目录和文件属性