【数据结构与算法】第十七、十八章:加权无向图、最小生成树(切分定理、贪心算法、Prim算法、kruskal算法)

本文主要是介绍【数据结构与算法】第十七、十八章:加权无向图、最小生成树(切分定理、贪心算法、Prim算法、kruskal算法),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

17、加权无向图

加权无向图是一种为每条边关联一个权重值或是成本的图模型。

这种图能够自然地表示许多应用。

在一副航空图中,边表示航线,权值则可以表示距离或是费用。

在一副电路图中,边表示导线,权值则可能表示导线的长度即成本,或是信号通过这条线所需的时间。

此时很容易就能想到,最小成本的问题,例如,从西安飞纽约,怎样飞才能使时间成本最低或者是金钱成本最低?

在下图中,从顶点0到顶点4有三条路径,分别为0-2-3-4,0-2-4,0-5-3-4,通过哪条路径到达4顶点最好呢?

此时就要考虑,那条路径的成本最低

在这里插入图片描述

17.1、边的表示

加权无向图中的边不能简单的使用v-w两个顶点表示了,而必须要给边 关联一个权重值,因此可以使用 对象 来描述一条边

在这里插入图片描述

17.2、加权无向图的实现

  • API

在这里插入图片描述

  • 代码
package chapter17;import chapter03.Queue;/*** @author 土味儿* Date 2021/9/16* @version 1.0* 加权无向图*/
public class EdgeWeightedGraph {/*** 顶点数量*/private final int vNum;/*** 边数量*/private int eNum;/*** 邻接表*/private Queue<Edge>[] adj;/*** 构造器* @param vNum*/public EdgeWeightedGraph(int vNum) {// 初始化顶点数量this.vNum = vNum;// 初始化边数量this.eNum = 0;// 初始化邻接表this.adj = new Queue[vNum];// 初始化邻接表中的空队列for (int i = 0; i < vNum; i++) {this.adj[i] = new Queue<Edge>();}}/*** 得到顶点数量* @return*/public int getVNum(){return vNum;}/*** 得到边数量* @return*/public int geteNum(){return eNum;}/*** 添加一条边v-w* @param e*/public void addEdge(Edge e){// 因为是无向图,让边e同时出现在e的两个顶点的邻接表中int v = e.either();int w = e.other(v);this.adj[v].enQueue(e);this.adj[w].enQueue(e);// 边数量加1eNum++;}/*** 获取顶点v的所有相邻顶点* @param v* @return*/public Queue<Edge> adj(int v){return this.adj[v];}/*** 获取加权无向图中的所有边* @return*/public Queue<Edge> edges(){// 创建一个队列对象,存储所有的边Queue<Edge> allEdges = new Queue<>();// 遍历图中的每一个顶点,找到每个顶点的邻接表,邻接表中存储了该顶点关联的每一条边for(int v=0;v<vNum;v++){// 遍历顶点v的邻接表,找到每一条和v关联的边for (Edge e : adj(v)) {// 每条边的两个顶点,一大一小,判断大小再添加,可以避免重复if(e.other(v) < v){allEdges.enQueue(e);}}}return allEdges;}
}

18、最小生成树

之前学习的加权图,它的边关联了一个权重,那么可以根据这个权重解决最小成本问题,但如何才能找到最小成本对应的顶点和边呢?最小生成树相关算法可以解决。

18.1、定义及相关约定

  • 图的生成树 是它的一棵含有其 所有顶点无环连通子图,一副加权无向图的最小生成树,是它的一棵权值(树中所有边的权重之和)最小的生成树

在这里插入图片描述

  • 只考虑连通图

    最小生成树的定义说明它只能存在于连通图中, 如果图不是连通的,那么分别计算每个连通图子图的最小生成树,合并到一起称为 最小生成森林

在这里插入图片描述

  • 所有边的权重都各不相同

    如果不同的边权重可以相同,那么一副图的最小生成树就可能不唯一了,虽然算法可以处理这种情况,但为了好理解,约定所有边的权重都各不相同

18.2、最小生成树原理

1)树的性质

  • 用一条边连接树中的任意两个顶点都会产生 一个新的环

在这里插入图片描述

  • 从树中删除任意一条边,将会得到 两棵独立的树

在这里插入图片描述

2)切分定理

要从一副连通图中找出该图的最小生成树,需要通过切分定理完成

  • 切分

    将图的所有顶点按照某些规则分为两个 非空没有交集 的集合

  • 横切边

    连接两个属于不同集合的顶点的边称之为横切边

例如:

将图中的顶点切分为两个集合,灰色顶点属于一个集合,白色顶点属于另外一个集合,那么效果如下:

在这里插入图片描述

  • 切分定理

    在一副加权图中,给定任意的切分,它的 横切边中的权重最小者 必然属于图中的最小生成树

在这里插入图片描述

  • 注意

    一次切分产生的多个横切边中,权重最小的边不一定是所有横切边中唯一属于图的最小生成树的边

在这里插入图片描述

3)贪心算法

贪心算法是计算图的最小生成树的基础算法,它的基本原理就是 切分定理

使用切分定理找到最小生成树的一条边,不断的重复直到找到最小生成树的所有边

如果图有 V 个顶点,那么需要找到 V-1 条边,就可以表示该图的最小生成树

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

计算图的最小生成树的算法有很多种,但这些算法都可以看做是贪心算法的一种特殊情况,这些算法的不同之处在于 保存切分判定权重最小的横切边 的方式

4)Prim算法

Prim算法,它的每一步都会为一棵生成中的树添加一条边。一开始这棵树只有一个顶点,然后会向它添加V-1条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入到树中

  • Prim算法的切分规则

    把最小生成树中的顶点看做是一个集合,把不在最小生成树中的顶点看做是另外一个集合

在这里插入图片描述

1、Prim算法API设计

在这里插入图片描述

2、Prim算法的实现原理

Prim算法始终将图中的顶点切分成两个集合,最小生成树顶点非最小生成树顶点,通过不断的重复做某些操作,可以逐渐将非最小生成树中的顶点加入到最小生成树中,直到所有的顶点都加入到最小生成树中

在设计API的时候,使用最小索引优先队列存放树中顶点与非树中顶点的有效横切边,那么它是如何表示的
呢?

可以让最小索引优先队列的 索引值表示图的顶点,让最小索引优先队列中的 表示从其他某个顶点到当前顶点的 边权重

在这里插入图片描述

初始化状态,先默认0是最小生成树中的唯一顶点,其他的顶点都不在最小生成树中,此时横切边就是顶点0的邻接表中0-2,0-4,0-6,0-7这四条边,只需要将索引优先队列的2、4、6、7索引处分别存储这些边的权重值就可以表示了。

现在只需要从这四条横切边中找出权重最小的边,然后把对应的顶点加进来即可。所以找到0-7这条横切边的权重最小,因此把0-7这条边添加进来,此时0和7属于最小生成树的顶点,其他的不属于,现在顶点7的邻接表中的边也成为了横切边,这时需要做两个操作:

1、0-7这条边已经不是横切边了,需要让它失效:只需要调用最小索引优先队列的delMin()方法即可完成

2、2和4顶点各有两条连接指向最小生成树,需要只保留一条:4-7的权重小于0-4的权重,所以保留4-7,调用索引优先队列的change(4,0.37)即可,0-2的权重小于2-7的权重,所以保留0-2,不需要做额外操作

在这里插入图片描述

不断重复上面的动作,就可以把所有的顶点添加到最小生成树中

3、代码
package chapter18;import chapter03.Queue;
import chapter07.IndexMinPriorityQueue;
import chapter17.Edge;
import chapter17.EdgeWeightedGraph;/*** @author 土味儿* Date 2021/9/17* @version 1.0* 最小生成树prim算法*/
public class PrimMST {/*** 最小边* 索引代表顶点* 值表示当前顶点到最小生成树之间的最小边*/private Edge[] edgeTo;/*** 最小边的权重* 索引代表顶点* 值表示当前顶点到最小生成树之间的最小边的权重*/private double[] distTo;/*** 索引代表顶点* 如果当前顶点已经在树中,则值为true,否则为false*/private boolean[] marked;/*** 存放树中顶点与非树中顶点的有效横切边*/private IndexMinPriorityQueue<Double> pq;/*** 构造器* 根据加权无向图创建最小生成树* @param g*/public PrimMST(EdgeWeightedGraph g) {// 初始化edgeTothis.edgeTo = new Edge[g.getVNum()];// 初始化distTothis.distTo = new double[g.getVNum()];for (int i = 0; i < g.getVNum(); i++) {// 默认值:正无穷大this.distTo[i] = Double.POSITIVE_INFINITY;}// 初始化markedthis.marked = new boolean[g.getVNum()];// 初始化pqpq = new IndexMinPriorityQueue<Double>(g.getVNum());// 默认让顶点0进入到树中,但是树中只有一个顶点0,因此,顶点0没有和任何顶点相连,所以distTo对应位置处的值为0.0distTo[0] = 0.0;pq.insert(0,0.0);// 遍历索引最小优先队列,拿到最小横切边对应的顶点,把该顶点加入到最小树中while(!pq.isEmpty()){visit(g, pq.delMin());}}/*** 获取最小生成树的所有边* @return*/public Queue edges(){// 创建队列对象Queue<Edge> allEdges = new Queue<>();// 把edgeTo中非null的值加入队列for (int i = 0; i < edgeTo.length; i++) {if(edgeTo[i] != null){allEdges.enQueue(edgeTo[i]);}}return allEdges;}/*** 将顶点v添加到最小生成树中,并更新数据* @param g* @param v*/private void visit(EdgeWeightedGraph g,int v){// 把顶点v添加到最小生成树中marked[v] = true;// 更新数据for (Edge e : g.adj(v)) {// 获取边e的另外一个顶点w(当前顶点v)int w = e.other(v);// 判断另一个顶点是否在树中,如果在树中,不做处理;如果不在树中,更新数据if(marked[w]){// 继续下次循环;不是退出,不能用breakcontinue;}// 比较权重:判断边e的权重是否小于 w到最小树中已存在的最小边的权重if(e.getWeight() < distTo[w]){// 更新数据edgeTo[w] = e;distTo[w] = e.getWeight();if(pq.contains(w)){pq.changeItem(w, e.getWeight());}else{pq.insert(w, e.getWeight());}}}}
}
  • 测试
package chapter18;import chapter03.Queue;
import chapter17.Edge;
import chapter17.EdgeWeightedGraph;
import org.junit.Test;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;/*** @author 土味儿* Date 2021/9/17* @version 1.0* 测试prim最小生成树*/
public class PrimMSTTest {@Testpublic void test() throws IOException {// 准备一幅加权无向图BufferedReader br = new BufferedReader(new InputStreamReader(PrimMSTTest.class.getClassLoader().getResourceAsStream("min_create_tree_test.txt")));// 顶点数量int total = Integer.parseInt(br.readLine());// 加权无向图EdgeWeightedGraph g = new EdgeWeightedGraph(total);// 边的数量int edges = Integer.parseInt(br.readLine());// 依次读取边for (int i = 0; i < edges; i++) {String line = br.readLine();String[] s = line.split(" ");int v = Integer.parseInt(s[0]);int w = Integer.parseInt(s[1]);double weight = Double.parseDouble(s[2]);// 创建加权无向边Edge edge = new Edge(v, w, weight);// 向图中加入边g.addEdge(edge);}// 创建prim最小生成树对象PrimMST primMST = new PrimMST(g);// 得到最小生成树Queue<Edge> allEdges = primMST.edges();// 输出for (Edge e : allEdges) {int x = e.either();int y = e.other(x);double w = e.getWeight();System.out.println(x + " - " + y + " : " + w);}}
}
  • min_create_tree_test.txt
8
16
4 5 0.35
4 7 0.37
5 7 0.28
0 7 0.16
1 5 0.32
0 4 0.38
2 3 0.17
1 7 0.19
0 2 0.26
1 2 0.36
1 3 0.29
2 7 0.34
6 2 0.40
3 6 0.52
6 0 0.58
6 4 0.93
  • 运行结果
1 - 7 : 0.19
0 - 2 : 0.26
2 - 3 : 0.17
4 - 5 : 0.35
5 - 7 : 0.28
6 - 2 : 0.4
0 - 7 : 0.16

5)kruskal算法

  • kruskal算法 是计算一副加权无向图的最小生成树的另外一种算法,它的主要思想是按照边的权重(从小到大)处理它们,将边加入最小生成树中,加入的边 不会 与已经加入最小生成树的边构成 ,直到树中含有 V-1 条边为止

  • kruskal算法和prim算法的区别

    • Prim算法是一条边一条边的构造最小生成树,每一步都为一棵树添加一条边
    • kruskal算法构造最小生成树的时候,也是一条边一条边地构造,但它的 切分规则是不一样的。它每一次寻找的边会连接一片森林中的两棵树;如果一副加权无向图由V个顶点组成,初始化情况下每个顶点都构成一棵独立的树,则V个顶点对应V棵树,组成一片森林,kruskal算法每一次处理都会将两棵树合并一棵树,直到整个森林中只剩一棵树为止

在这里插入图片描述

1、kruskal算法API设计

在这里插入图片描述

2、kruskal算法的实现原理

在设计API的时候,使用了一个MinPriorityQueue pq存储图中所有的边,每次使用 pq.delMin() 取出权重最小的边,并得到该边关联的两个顶点v和w,通过uf.connect(v,w)判断v和w是否已经连通

如果连通,则证明这两个顶点在同一棵树中,那么就不能再把这条边添加到最小生成树中,因为在一棵树的任意两个顶点上添加一条边,都会形成环,而最小生成树不能有环的存在

如果不连通,则通过uf.connect(v,w)把顶点v所在的树和顶点w所在的树合并成一棵树,并把这条边加入到mst队列中,这样如果把所有的边处理完,最终mst中存储的就是最小生树的所有边

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3、代码
package chapter18;import chapter03.Queue;
import chapter07.MinPriorityQueue;
import chapter13.UF_Tree_Weighted;
import chapter17.Edge;
import chapter17.EdgeWeightedGraph;/*** @author 土味儿* Date 2021/9/17* @version 1.0* 最小生成树Kruskal算法*/
public class KruskalMST {/*** 保存最小生成树所有边*/private Queue<Edge> edges;/*** 优化并查集* 索引代表顶点* 使用uf.connect(v,w)可以判断顶点v和顶点w是否在同一颗树中* 使用uf.union(v,w)可以把顶点v所在的树和顶点w所在的树合并*/private UF_Tree_Weighted uf;/*** 存储图中所有的边* 使用最小优先队列,对图中的边进行排序*/private MinPriorityQueue<Edge> pq;/*** 构造器* 根据一幅加权无向图,创建最小生成树** @param g*/public KruskalMST(EdgeWeightedGraph g) {// 初始化边队列this.edges = new Queue<Edge>();// 初始化并查集this.uf = new UF_Tree_Weighted(g.getVNum());// 初始化最小优先队列this.pq = new MinPriorityQueue<>(g.getENum());// 把图中所有的边存储到pq中for (Edge e : g.edges()) {this.pq.insert(e);}// 找到权重最小的边,并处理// 最小生成树中边的个数 等于 顶点个数减1int i=0;while (!pq.isEmpty() && edges.size() < g.getVNum() - 1) {// 找出最小边Edge e = pq.delMin();// 找出边的两个点int v = e.either();int w = e.other(v);// 判断这两个点是否在同一棵树中if (uf.connected(v, w)) {// 在同一棵树中,继续下次循环continue;}// 不在同一棵中,把两个点合并uf.union(v, w);// 把当前边加入最小树队列中this.edges.enQueue(e);}}/*** 获取最小生成树的所有边** @return*/public Queue<Edge> getEdges() {return this.edges;}
}

这篇关于【数据结构与算法】第十七、十八章:加权无向图、最小生成树(切分定理、贪心算法、Prim算法、kruskal算法)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/942795

相关文章

IDEA自动生成注释模板的配置教程

《IDEA自动生成注释模板的配置教程》本文介绍了如何在IntelliJIDEA中配置类和方法的注释模板,包括自动生成项目名称、包名、日期和时间等内容,以及如何定制参数和返回值的注释格式,需要的朋友可以... 目录项目场景配置方法类注释模板定义类开头的注释步骤类注释效果方法注释模板定义方法开头的注释步骤方法注

Python如何自动生成环境依赖包requirements

《Python如何自动生成环境依赖包requirements》:本文主要介绍Python如何自动生成环境依赖包requirements问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑... 目录生成当前 python 环境 安装的所有依赖包1、命令2、常见问题只生成当前 项目 的所有依赖包1、

openCV中KNN算法的实现

《openCV中KNN算法的实现》KNN算法是一种简单且常用的分类算法,本文主要介绍了openCV中KNN算法的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录KNN算法流程使用OpenCV实现KNNOpenCV 是一个开源的跨平台计算机视觉库,它提供了各

MySQL中动态生成SQL语句去掉所有字段的空格的操作方法

《MySQL中动态生成SQL语句去掉所有字段的空格的操作方法》在数据库管理过程中,我们常常会遇到需要对表中字段进行清洗和整理的情况,本文将详细介绍如何在MySQL中动态生成SQL语句来去掉所有字段的空... 目录在mysql中动态生成SQL语句去掉所有字段的空格准备工作原理分析动态生成SQL语句在MySQL

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

Java利用docx4j+Freemarker生成word文档

《Java利用docx4j+Freemarker生成word文档》这篇文章主要为大家详细介绍了Java如何利用docx4j+Freemarker生成word文档,文中的示例代码讲解详细,感兴趣的小伙伴... 目录技术方案maven依赖创建模板文件实现代码技术方案Java 1.8 + docx4j + Fr

Java编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

使用Jackson进行JSON生成与解析的新手指南

《使用Jackson进行JSON生成与解析的新手指南》这篇文章主要为大家详细介绍了如何使用Jackson进行JSON生成与解析处理,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 核心依赖2. 基础用法2.1 对象转 jsON(序列化)2.2 JSON 转对象(反序列化)3.

C#数据结构之字符串(string)详解

《C#数据结构之字符串(string)详解》:本文主要介绍C#数据结构之字符串(string),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录转义字符序列字符串的创建字符串的声明null字符串与空字符串重复单字符字符串的构造字符串的属性和常用方法属性常用方法总结摘

java中使用POI生成Excel并导出过程

《java中使用POI生成Excel并导出过程》:本文主要介绍java中使用POI生成Excel并导出过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录需求说明及实现方式需求完成通用代码版本1版本2结果展示type参数为atype参数为b总结注:本文章中代码均为