轻松学懂图(下)——Dijkstra和Bellman-Ford算法

2023-10-31

概述

​ 在上一篇文章中讲述了Kruskal和Prim算法,用于得到最小生成树,今天将会介绍两种得到最短路径的算法——Dijlkstra和Bellman-Ford算法

Dijkstra算法

  • 算法的特点
    • 属于单源最短路径算法,什么是单源呢,通俗的说也就是一个起点,该算法一次只能得到一个点到其他点的最短路径。
    • 限制条件:图中不能有负权边。也就是图中不能有权值为负数的边

上面的特点在我讲完这个算法的思想之后你就会明白为什么了。

  • 算法思想

    这个算法的思想其实特别贴近生活,如果你把每个点都想象成一个小石头,每个边都想成一根绳子,边的权值表示绳长。然后,我们选择其中一个石头,作为第一个从地上被拉起来的,将其慢慢地从地上拿起来,之后肯定会有一个离它最近的小石头也会被拉起来,就这样不断往上提,最终所有石头都会被提起来。好了,说完了这个例子后,我想问你一个比较常识性的问题:除了第一块石头外,每块被拉起来的石头是否都是被前一个被拉起来的石头所拉起来的?当你相通了这个,后面当我解释过程的时候就好说了

    我们先来看一下下面这个例子:我们以A为起点,按照我们之前的例子去挨个提起来:

在这里插入图片描述

​ 过程:

在这里插入图片描述

上面是使用该算法的一个过程图,我们现在来对其进行一个解释,并且,为了方便记录最短路径,我会用一个表来记录最短路径

  1. 以A为源点,并且将A点所直接指向的顶点的路径信息记录到该表中。从图中不难看出提起来的第一块石头是B,它是被A提起来的,因此更新A到B的最短路径信息,我们发现和原来是一样的

    终点 最短路径 长度
    B A -> B 10
    C
    D A -> D 30
    E A -> E 100
  2. 接下来,下一个被踢起来的是D,它是被A提起来的,更新其最短路径,更新后的路径信息也是和原来一样

    终点 最短路径 长度
    B A -> B 10
    C
    D A -> D 30
    E A -> E 100
  3. 很明显,下一个被踢起来的是C,它是被谁提起来的呢?看一下图就知道,它是被D所提起来的。为什么我总是强调是被谁提起来的这个问题呢?还记得我之前在说思想的时候说过的问题吗?正是因为每个被提起来的石头都是被上一个已经被提起的石头所提起来的,所以他的最短路径就是,提起他的那块石头的最短路径再到达当前被提起的石头的最短路径就是当前石头的最短路径了。

    如果听着有点蒙,我们用这一步的这个例子给你说明一下,当前C被D所提起来,因此A到C的最短路径就是A到D的最短路径加上D到C的最短路径,权值也是在上一条路径的基础上进行相加得到的。我们发现此时的路径信息50要小于一开始的路径长度∞,因此更新后最短路径信息就变成了下面这样了

    终点 最短路径 长度
    B A -> B 10
    C A -> D -> C 50
    D A -> D 30
    E A -> E 100
  4. 再往后就是最后一步了,E点被提起,它是被C所提起来的,并且此时的最短路径长度比表中的原来的路径长度要短,因此进行更新

    终点 最短路径 长度
    B A -> B 10
    C A -> D -> C 50
    D A -> D 30
    E A -> D -> C -> E 60
  5. 至此,就得到了以A为起点到达其他点的最短路径信息了

  • 参考代码(部分)

    - 
    
        /**
             * Dijkstra算法,原理:每个节点当做石头,每个边当做绳子,然后把往上拉
             * 
             * 限制条件:图中不能有负权边,不然可能会出现,某个点被提起来之后,又发现了可以到达它的更短的边
             * 也可以理解为,在选起点后,就存在某条权值为负数的路径,就是说已经被提起来了
             *
             * @return V - PathInfo
             * PathInfo中存的是List<EdgeInfo<V, E> 和 weight
             */
            @Override
            public Map<V, PathInfo<V, E>> dijkstra(V begin) {
                // 拿到起点
                Vertex<V, E> beginVertex = vertices.get(begin);
                if (Objects.isNull(beginVertex)) {
                    return null;
                }
                // 返回结果
                Map<V, PathInfo<V, E>> result = new HashMap<>();
                // 未拿起来的顶点
                Map<Vertex<V, E>, PathInfo<V, E>> paths = new HashMap<>();
        
                // 初始化paths里面一开始只有A->A weight=0这样一条路劲,然后进入下面的循环进行松弛
                paths.put(beginVertex, new PathInfo<>(weightManager.zero()));
        
                while (!paths.isEmpty()) {
                    // 选择一个最短路径所到达的顶点
                    Map.Entry<Vertex<V, E>, PathInfo<V, E>> minEntry = getMinPath(paths);
                    // 松弛操作
                    // 拿到顶点,即一块“石头”被提起
                    Vertex<V, E> minVertex = minEntry.getKey();
                    // 拿到当前最短边的PathInfo
                    PathInfo<V, E> minPathInfo = minEntry.getValue();
                    // 加入到result中
                    result.put(minVertex.value, minEntry.getValue());
                    // 从paths中删除
                    paths.remove(minVertex);
                    // 遍历该顶点的outEdges并试着更新边的to,这里遍历不能用foreach,否则不能continue
                    for (Edge<V, E> edge : minVertex.outEdges) {
                        // 为了适用于无向图,也要判断to指向的顶点是否已经被“提起”过,避免重复遍历
                        // 如果是begin则跳过,不能加到result中,在此处判断或者在返回前将其删除
                        if (result.containsKey(edge.to.value)) {
                            continue;
                        }
                        relaxForDijkstra(edge, paths, minPathInfo);
                    }
                }
                // 将起点从结果中移除
                result.remove(begin);
        
                return result;
            }
    
    

    代码中注释部分的松弛操作指的是提石头并更新最短路径信息。

  • 小结

    回顾之前的算法特点,我们回答一下为什么不能有负权边这个问题,其实就利用这个算法的提石头思想就可以解释,提石头的时候绳子的长度不能肯定不能是负的,如果是负的,那该算法就解决不了了,因为

Bellman-Ford算法

  • 算法特点

    • 也属于单源最短路径算法,能检测负权环(指的是存在至少一条负权边的环)是否存在
    • 支持负权边的存在
  • 算法思想

    • 和上面的提石头思想比起来,这个算法更像是把所有的路都走好几遍,走熟悉了也就知道怎么走最近了,这怎么理解呢?也可以理解为生活中的一类例子——见下图,比如,某人来到一个陌生的地方,他家住在A处,并且每周都会按照1、2、3、4、5的顺序座别人车经过某条路,并且在这个过程中它会留意从家出发到这里最近的路(一开始肯定是什么都不知道的),现在,他第一次来到这个陌生的地方,按照顺序,先座车从B到E,但是它现在都还不知道从家到B最近是多少,所以到E也就不知道,以此类推,直到第三次从A到C,它发现是从他家出发的,距离为5,此时他就知道了从A到C的最短路径是5,同理,到第五步的时候,他知道了从A到B的最短路径是9,接着到下一周,当经过从B到E这条路的时候,此时因为它知道从A到B的最短路径,所以,也就知道了从A到E的最短路径是17,接下来,同理,知道了从C到D的最短路径是8,,从A到E的最短路径是9,当走B到D这条路径的时候需要注意了,此时又发现了一条从A到B再到D的路径,长度是15,原来已经知道了从A到D的最短路径是8,因此最短路径不更新。就这样,直到有一周发现没有任何新的最短路径信息的时候,说明目前掌握的信息就是正确的最短路径信息了。这其实就是该算法的思想,通俗的说就是不断的去试

在这里插入图片描述

  • 代码(部分)

    public Map<V, PathInfo<V, E>> bellmanFord(V begin) {
            // 拿到起点
            Vertex<V, E> beginVertex = vertices.get(begin);
            if (Objects.isNull(beginVertex)) {
                return null;
            }
    
            // 初始化: 返回结果
            Map<V, PathInfo<V, E>> result = new HashMap<>();
            // 初始化起点,并放入result中
            PathInfo<V, E> beginPathInfo = new PathInfo<>();
            beginPathInfo.weight = weightManager.zero();
            result.put(begin, beginPathInfo);
    
            // 因为每次最少会找到一条,所以理论上最多循环V-1次就可以找到所有的
            int count = vertices.size() - 1;
            PathInfo<V, E> fromPathInfo;
            for (int i = 0; i < count; i++) {
                for (Edge<V, E> edge : edges) {
                    // 松弛操作,最开始这里可能为空,因此需要初始化起点并放入result
                    fromPathInfo = result.get(edge.from.value);
                    relax(edge, result, fromPathInfo);
                }
            }
    
            // 再循环一次,判断是否有负权环,如果还有边可以进行松弛操作说明存在负权环
            for (Edge<V, E> edge : edges) {
                // 松弛操作,最开始这里可能为空,因此需要初始化起点并放入result
                fromPathInfo = result.get(edge.from.value);
                if (relax(edge, result, fromPathInfo)) {
                    System.out.println("存在负权环");
                    return null;
                }
            }
    
            // 将起点从结果中移除
            result.remove(begin);
            return result;
        }
    

    ​ 解释: 该算法的原理就是按照一定的顺序将所有的路径不断的重复遍历,每次遍历都会通过搜对边进行松弛操作来发现最少一条最短路径,直到某次循环没有再发现新的最短路径的时候,这个时候说明最短路径已经都找到了,次时算法结束。也许你会对这个结束的条件有点疑惑,我们可以举个例子,假如某次循环更新了一条最短路径,那么是不是有可能该路径后面的路径也会发生变化呢?是不是要再次去遍历?当然要!

    和上面的Dijkstra算法相似的是,都会维护一个数据结构用于保存最短路径信息,并且都是不断地去遍历路径。不一样的是遍历的方式有所不同,对于前者,每次找确定的边的时候都是找最短的那一条,确定了的边下次不会再去循环。对于后者,就按照一定的顺序去进行松弛即可,实现起来相对较简单。

    还有一个问题还没有说,就是Bellman-Ford算法是如何检测负权环的?我们不妨先假设,有负权环 会怎么样:首先,负权环因为环路的总权值是负的,所以每走一圈,路径长度就会减少,每次松弛都会减 少,因此每次循环都会伴随着至少一次以上的最短路径信息更新操作。之前我们也说了,每次循环至少会 找到一条最短路径,那么如果要找到所有的最短路径,那么遍历的次数一定是有限的!,但是有负权环的 情况下就回无限循环,所以我们可根据循环次数来判断是否存在负权环!

总结

我们不妨先假设,有负权环 会怎么样:首先,负权环因为环路的总权值是负的,所以每走一圈,路径长度就会减少,每次松弛都会减 少,因此每次循环都会伴随着至少一次以上的最短路径信息更新操作。之前我们也说了,每次循环至少会 找到一条最短路径,那么如果要找到所有的最短路径,那么遍历的次数一定是有限的!,但是有负权环的 情况下就回无限循环,所以我们可根据循环次数来判断是否存在负权环!**

总结

​ 本次学习了Dijkstra算法和Bellman-Ford算法,两种算法各有各的特点,前者不能存在负权边,后者可以允许负权边,并且可以检测负权环。前者是提石头的思想,后者则是不断的去重复走,直至熟悉所有最短路径

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

轻松学懂图(下)——Dijkstra和Bellman-Ford算法 的相关文章

随机推荐

  • 【VUE】在vue项目实践当中使用swiper轮播图教程

    步骤 1 安装vue awesome swiper npm install vue awesome swiper S 2 在vue项目中引用vue awesome swiper main js import VueAwesomeSwiper
  • Java是面向过程语言还是面向对象语言?

    目录 一 面向过程语言 二 面向对象语言 三 Java是面向过程语言还是面向对象语言 一 面向过程语言 面向过程语言是一种编程范式 它将程序设计看作是按照一定的步骤或流程进行处理的过程 在面向过程语言中 程序员需要自己定义数据结构和算法 并
  • linux的链接方式

    linux的硬链接和软链接 1 链接的概念 Linux链接分两种 一种被称为硬链接 Hard Link 另一种被称为软链接也叫符号链接 Symbolic Link 默认情况下 ln命令产生硬链接 2 硬链接 在Linux文件系统当中 保存在
  • 年入50万,程序员的第二条赛道

    大家好 我是厂长 我有个朋友 叫佩佩 这几年我亲眼见证了她从月薪6千 到年入百万 她曾经靠一条短视频带货就赚了30万佣金 最近 我看到她在做小红书无货源电商这个风口项目 两个月就做到了单店30万的战绩 她说2023年是小红书电商元年 0粉
  • 回味C语言

    虽然在实际上没怎么使用C语言了 但是看到C语言的书总是忍不住想看一下 喜欢这种至简 却又有着强大能力的语言 读完书随手写的一些笔记 略有些简单 书还是很喜欢的 推荐给大家 C专家编程 第一章 C 穿越时空的迷雾 原型决定C语言不支持函数重载
  • 小程序面试题

    1 简单描述一下微信小程序的相关文件类型 答 WXML WeiXin Markup Language 是框架设计的一套标签语言 结合基础组件 事件系统 可以构建出页面的结构 内部主要是微信自己定义的 套组件 WXSS WeiXin Styl
  • mysql 利用binlog增量备份,还原实例(日志备份数据库)

    一 什么是增量备份 增量备份 就是将新增加的数据进行备份 假如你一个数据库 有10G的数据 每天会增加10M的数据 数据库每天都要备份一次 这么多数据是不是都要备份呢 还是只要备份增加的数据呢 很显然 我只要备份增加的数据 这样减少服务器的
  • C++ 调用python

    本文代码已在vs2017上验证 c 调用python需要三类文件 这些文件都可以在python安装目录下找到 1 include文件夹 位于python目录下 2 dll文件 位于python目录下 如python37 dll 3 lib文
  • 超分辨率概述

    1 什么是超分辨率增强 Video super resolution is the task of upscaling a video from a low resolution to a high resolution 超分辨率 Supe
  • Git & GitHub 入门6:用好commit message

    git log 可以查看所有的 commit messages 修改repo中的文件内容后 add该文件 直接运行命令git commit进入message编辑状态 可以输入多行commit message说明 完成后点击ECS键退出编辑
  • Gin-swaggo为gin框架提供Swagger 文档

    官方 https github com swaggo gin swagger 开始使用 为API方法增加注释 加在controller api 层 See Declarative Comments Format 运行下面命令下载swgo g
  • L2-4 部落PTA

    在一个社区里 每个人都有自己的小圈子 还可能同时属于很多不同的朋友圈 我们认为朋友的朋友都算在一个部落里 于是要请你统计一下 在一个给定社区中 到底有多少个互不相交的部落 并且检查任意两个人是否属于同一个部落 输入格式 输入在第一行给出一个
  • hadoop3.2.1编译安装

    基础环境 centos 7 7 三台 hadoop需要的环境 Requirements Unix System JDK 1 8 Maven 3 3 or later ProtocolBuffer 2 5 0 CMake 3 1 or new
  • echart 折线图设置y轴单位_如何让echarts中y轴的单位位于数值的右上角

    展开全部 1 创建折线图的数据区 包括年份和数据 2 仅选择数据区创建折线图 插入选项卡 图表62616964757a686964616fe78988e69d8331333363396364工具组 折线图 3 得到的折线图x坐标不满足要求
  • c++可变参数模板函数

    可变参数模版函数 类型一致 可变参数 使用头文件 cstdarg va list arg ptr 开头指针 va start arg ptr n 从开头开始读取n个 va arg arg ptr T 根据数据类型取出数据 va end ar
  • jdk1.8升级后 sun.io.CharToByteConverter 错误处理

    项目工程中用到jdk1 6相关方法 可以使用 但是升级到jdk1 8以后 编译出现java lang NoClassDefFoundError sun io CharToByteConverter错误 后经查询 是jdk1 8版本中已经从s
  • 前端02:CSS选择器等基础知识

    CSS基础选择器 设置字体样式 文本样式 CSS的三种引入方式 能使用Chrome调试工具调试样式 HTML专注做结构呈现 样式交给CSS 即结构 HTML 和样式CSS相分离 CSS主要由量分布构成 选择器以及一条或多条声明 选择器 给谁
  • 深度学习10篇文章之Interleaved Group Convolution

    本文主要讲解Ting Zhang的Interleaved Group Convolutions for Deep Neural Networks 该文对Group convolution有较为详细的讲解 Abstract 文章开篇引出了 I
  • 新昌中学2021高考成绩查询,2021绍兴市地区高考成绩排名查询,绍兴市高考各高中成绩喜报榜单...

    距离2018年高考还有不到一个月的时间了 很多人在准备最后冲刺的同时 也在关心高考成绩 2018各地区高考成绩排名查询 高考各高中成绩喜报榜单尚未公布 下面是往年各地区高考成绩排名查询 高考各高中成绩喜报榜单 想要了解同学可以参考下 同时关
  • 轻松学懂图(下)——Dijkstra和Bellman-Ford算法

    概述 在上一篇文章中讲述了Kruskal和Prim算法 用于得到最小生成树 今天将会介绍两种得到最短路径的算法 Dijlkstra和Bellman Ford算法 Dijkstra算法 算法的特点 属于单源最短路径算法 什么是单源呢 通俗的说