最短路算法——Dijkstra

2023-11-07

Dijkstra


在大多数最短路径问题中,Dijkstra 算法是最常用、效率最高的。它是一种“单源”最短路径算法,一次计算能得到从一个起点 s 到其他所有点的最短距离长度、最短路径的途径点。

一、Dijkstra的算法思想

Dijkstra 的模型例如多米诺骨牌,你可以想象下面的场景:

在图中所有的边上,排满多米诺骨牌,相当于把骨牌看成图的边。一条边上的多米诺骨牌数量,和边的权值(例如长度或费用)成正比。规定所有骨牌倒下的速度都是一样的。如果在一个结点上推倒骨牌,会导致这个结点上的所有骨牌都往后面倒下去。

        当我们在起点 s 推倒骨牌,可以观察到,从 s 开始,它连接的边上的骨牌都逐渐倒下,并到达所有能达到的结点。在某个结点 t ,可能先后从不同的线路倒骨牌过来;先倒过来的骨牌,其经过的路径,肯定就是从 s 到达 t 的最短路;后倒过来的骨牌,对确定结点 t 的最短路没有贡献,不用管它。从整体看,这是一个起点 s 扩散到整个图的过程。

而在这个过程中,观察所有结点的最短路径是这样得到的:

  1. 在 s 的所有直连邻居中,最近的邻居 u,骨牌首先到达。u 是第一个确定最短路径的结点。从 u 直连到 s 的路径肯定是最短的,因为如果 u 绕道别的结点到 s,必然更远。
  2. 把后面骨牌的倒下分成 2 部分,一部分是从 s 继续倒下到 s 的其它的直连邻居,另一部分从 u 出发倒下到 u 的直连邻居。那么下一个到达的结点 v,必然是 s 或者 u 的一个直连邻居。v 是第二个确定最短路径的结点。
  3. 继续以上步骤,在每一次迭代过程中,都能确定一个结点的最短路径。

我们用下面的表来总结 Dijkstra 算法的基本过程:

步骤 做法 具体操作 结果
1 从起点s出发,用BFS扩展它的邻居结点。 把这些邻居点放到一个集合A中,并记录这些点到s的距离。
2 选择距离s最近的那个邻居v,继续用BFS扩展v的邻居 (1)在A中找到距离s最小的点v,把v的邻居点,放到A中;
(2)如果v的邻居经过v中转,到s的距离更短,则更新这些邻居到s的距离;
(3)从集合A中移走v,后面不再处理v。
(1)得到了从s到v的最短路;
(2)v的邻居更新了到s的距离。
3 重复步骤2,直到所有点都扩展到并计算完毕 集合A为空。计算出了所有点到s的最短距离

        Dijkstra算法应用了贪心法的思想,即“抄近路走,肯定能找到最短路径”。算法可以简单概况为:Dijkstra = BFS + 贪心。实际上,“Dijkstra + 优先队列 = BFS + 优先队列(队列中的数据是从起点到当前点的距离)” 

        我们来分析一下Dijkstra 的复杂度:设图的点有 n 个,边有 m 条。编码的时候,集合 A 一般用优先队列来模拟。优先队列可以用堆或其他高效的数据结构实现,往优先队列中插入一个数、取出最小值的操作都是 O(logn) 的。一共往队列中插入 m 次(每条边都要进集合 A 一次),取出 n 次(每次从集合 A 中取出距离 s 最短的一个点,取出时要更新这个点的所有邻居到 s 的距离,设一个点平均有 k 个邻居),那么总复杂度是 O(m×logn+n×k×logn) ≈ O(m×logn),一般有 m 大于 n。不过要注意,在稠密图情况下 m 是 O(n^2) ,k 是 O(n)的。在计算单源最短路时,Dijkstra 是效率最高的算法。

       Dijkstra 存图使用的数据结构:题目若是稀疏图,往往 n 很大而 m 小,必须使用邻接表、链式前向星来存图;若是稠密图则 n 较小,就用简单的邻接矩阵,用邻接表也并不能减少存储空间。

        Dijkstra 的高效稳定: 从集合 AA 中得到一个点的最短路后,继续 BFS 时只需要扩展和更新这个点的邻居,范围很小,算法是高效的;而每次从集合A中都能得到一个点的最短路,算法是稳定的。 

        但Dijkstra的边的权值不能为负数。因为 Dijkstra 基于BFS,计算过程是从起点 s 逐步往外扩散的过程,每扩散一次就用贪心得到到一个点的最短路。扩散要求路径越来越长,如果遇到一个负权边,会导致路径变短,使扩散失效。见下图,设当前得到 s→u 的最短路,路径长度为 8,此时 s→u 的路径计算已经结束了。继续扩展 u 的邻居,若 u 到邻居 v 的边权是 -15,而 v 到 s 的距离为 20,那么 u 存在另一条途径 v 到 s 的路径,距离为 20 + (-15) = 5,这推翻了前面已经得到的长度 8 的最短路,破坏了 BFS 的扩散过程。

二、Dijkstra的执行过程

        编程的主要内容是维护两个集合:已确定最短路径的结点集合 A 、这些结点向外扩散的邻居结点集合 B。程序逻辑是:

  1. 把起点 s 放到 A 中,把 s 所有的邻居放到 B 中。此时,邻居到 s 的距离就是直连距离。
  2. 从 B 中找出距离起点 s 最短的结点 u,放到 A 中。
  3. 把 u 所有的新邻居放到 B 中。显然,u 的每一条边都连接了一个邻居,每个新邻居都要加进去。其中 u 的一个新邻居 v,它到 s 的距离 dis(s,v) 等于 dis(s, u) + dis(u, v)。
  4. 重复步骤 2、3,直到 B 为空时,结束。

计算结束后,就可以得到从起点 s 到其它所有点的最短距离啦。 

举个栗子:如图,起点是 1,现在我们要求 1 到其它所有结点的最短路径。

步骤如下:

  1. 1 到自己的距离最短,把 1 放到集合 A 里:A={1}。把 1 的邻居放到集合 B里:B={(2-5), (3-2)}。其中 (2-5) 表示结点 2 到起点的距离是 5 。
  2. 从 B 中找到离集合 A 最近的结点,是结点 3。在 A 中加上 3,现在 A={1, 3},也就是说得到了从 1 到 3 的最短距离;从 B 中拿走 (3-2),现在 B={(2-5)}。
  3. 对结点 3 的每条边,扩展它的新邻居,放到 B 中。3 的新邻居是 2 和 4,那么 B={(2-5), (2-4), (4,7)}。其中 (2-4) 是指新邻居 2 通过 3 到起点 1,距离是 4。由于 (2-4) 比 (2-5)更好,丢弃 (2-5),B={(2-4), (4-7)}。
  4. 重复步骤 2、3。从 B 中找到离起点最近的结点,是结点 2。在 A 中加上 2,并从 B 中拿走 (2-4);扩展 2 的邻居放到 B 中。现在 A={1, 3, 2},B={(4-7), (4-5)}。由于 (4-5) 比 (4-7)更好,丢弃 (4-7),B={(4-5)}。
  5. 从 B 中找到离起点最近的结点,是结点 4。在 A 中加上 4,并从 B 中拿走(4-5)。已经没有新邻居可以扩展。现在 A={1, 3, 2, 4},B 为空,结束。

我们在看看这个过程的复杂度:

        我们设图的边共有 m 个,需要往集合 B 中扩展 m 次。在每次扩展后,需要找集合 B 中距离起点最小的结点。集合 B 最多可能有 n 个结点。把问题抽象为:每次往集合 B 放一个数据;然后在 B 中的 n 个数中找最小值......

        这个最小值要怎么找呢?

如果往 B 中放数据是乱放,找最小值也是用类似冒泡的简单方法,复杂度是 n,那么总复杂度是 O(nm),和 Bellman-Ford 一样。不过上述方法可以改进,得到更好的复杂度,改进方法是:

  1. 每次往 B 中放新数据时,按从小到大的顺序放,用二分法的思路,复杂度是 O(logn),保证最小的数总在最前面;
  2. 找最小值,直接取 B 的第一个数,复杂度是 O(1)。

        这样 Dijkstra 算法总的复杂度就是 O(mlogn),是最高效的最短路算法。而我们在编程时,一般不用自己写上面的程序,直接用 STL 的优先队列就可完成数据的插入和提取。 

三、Dijkstra经典题目练习


 点击 -> Dijkstra经典题目分析及参考代码


王国

题目描述

小明是王国的王子,今天是他登基之日。在即将成为国王之前,老国王给他出了道题,他想要考验小明是否有能力管理国家。

题目的内容如下:

王国一共有 N 个建筑和 M 条单向道路,每条道路都连接着两个建筑,每个建筑都有自己编号,分别为 1∼N 。(其中皇宫的编号为 1)

国王想让小明回答从皇宫到每个建筑的最短路径是多少,但紧张的小明此时已经无法思考,请你编写程序帮助小明回答国王的考核。

输入描述

输入第一行包含三个正整数 N,M。

第 2 到 M + 1 行每行包含三个正整数 u,v,w,表示 uv 之间存在一条距离为 w 的路。

1 ≤ ≤ 3×10^5,1 ≤ ≤ 10^6,1 ≤ ui​,vi​ ≤ N,0 ≤ wi​ ≤ 10^9。

输出描述

输出仅一行,共 N 个数,分别表示从皇宫到编号为 1∼N 建筑的最短距离,两两之间用空格隔开。(如果无法到达则输出 -1)

样例输入

3 3 
1 2 1
1 3 5
2 3 2

样例输出

0 1 3

如有错误和需要改进完善之处,欢迎大家纠正指教。

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

最短路算法——Dijkstra 的相关文章

  • 查找哪些页面不再与写入时复制共享

    假设我在 Linux 中有一个进程 我从中fork 另一个相同的过程 后forking 因为原始进程将开始写入内存 Linux写时复制机制将为进程提供与分叉进程使用的不同的唯一物理内存页 在执行的某个时刻 我如何知道原始进程的哪些页面已被写
  • 进程何时获得 SIGABRT(信号 6)?

    C 中进程获得 SIGABRT 的场景有哪些 该信号是否始终来自进程内部 或者该信号可以从一个进程发送到另一个进程吗 有没有办法识别哪个进程正在发送该信号 abort 向调用进程发送SIGABRT信号 就是这样abort 基本上有效 abo
  • OpenCv读/写视频色差

    我试图简单地使用 openCV 打开视频 处理帧并将处理后的帧写入新的视频文件 我的问题是 即使我根本不处理帧 只是打开视频 使用 VideoCapture 读取帧并使用 VideoWriter 将它们写入新文件 输出文件看起来比输入更 绿
  • 迭代变量并查找特定类型实例的技术

    我想迭代进程中内存中的变量 通过插件动态加载 并查找特定类型的实例 以前我可以找到特定类型 或内存中的所有类型 我可以创建类型的实例 我可以获取作为不同类型的字段包含的实例 但我无论如何都不知道只是 搜索 特定类型的实例 一种方法是使用 W
  • 使用具有现有访问令牌的 Google API .NET 客户端

    用例如下 移动应用程序正在通过 Google 对用户进行身份验证 并且在某些时候 我们需要将用户的视频发布到他的 YouTube 帐户 出于实际原因 实际发布应该由后端完成 已经存储在那里的大文件 由于用户已经通过应用程序的身份验证 因此应
  • 为什么我不能用 `= delete;` 声明纯虚函数?

    Intro 纯虚函数使用通用语法声明 virtual f 0 然而 自 c 11 以来 有一种方法可以显式地传达non existence 特殊 成员函数的 Mystruct delete eg default constructor Q
  • C++:重写已弃用的虚拟方法时出现弃用警告

    我有一个纯虚拟类 它有一个纯虚拟方法 应该是const 但不幸的是不是 该接口位于库中 并且该类由单独项目中的其他几个类继承 我正在尝试使用这个方法const不会破坏兼容性 至少在一段时间内 但我找不到在非常量方法重载时产生警告的方法 以下
  • 如何使用recv()检测客户端是否仍然连接(并且没有挂起)?

    我写了一个多客户端服务器程序C on SuSE Linux 企业服务器 12 3 x86 64 我为每个客户端使用一个线程来接收数据 我的问题是 我使用一个终端来运行服务器 并使用其他几个终端来运行服务器telnet到我的服务器 作为客户端
  • 如何配置 WebService 返回 ArrayList 而不是 Array?

    我有一个在 jax ws 上实现的 java Web 服务 此 Web 服务返回用户的通用列表 它运行得很好 Stateless name AdminToolSessionEJB RemoteBinding jndiBinding Admi
  • 访问者和模板化虚拟方法

    在一个典型的实现中Visitor模式 该类必须考虑基类的所有变体 后代 在许多情况下 访问者中的相同方法内容应用于不同的方法 在这种情况下 模板化的虚拟方法是理想的选择 但目前这是不允许的 那么 模板化方法可以用来解析父类的虚方法吗 鉴于
  • 将数据打印到文件

    我已经超载了 lt lt 运算符 使其写入文件并写入控制台 我已经为同一个函数创建了 8 个线程 并且我想输出 hello hi 如果我在无限循环中运行这个线程例程 文件中的o p是 hello hi hello hi hello hi e
  • 在非活动联合成员上使用“std::addressof”是否定义明确[重复]

    这个问题在这里已经有答案了 下面的代码是尝试实现constexpr的版本offsetof在 C 11 中 它可以在 gcc 7 2 0 和 clang 5 0 0 中编译 这取决于申请std addressof工会非活跃成员的成员 这是明确
  • 如何重置捕获像素的值

    我正在尝试创建一个 C 函数 该函数返回屏幕截图位图中每四个像素的 R G 和 B 值 这是我的代码的一部分 for int ix 4 ix lt 1366 ix ix 4 x x 4 for int iy 3 iy lt 768 iy i
  • 为什么我不应该对不是由 malloc() 分配的变量调用 free() ?

    我在某处读到 使用它是灾难性的free删除不是通过调用创建的对象malloc 这是真的 为什么 这是未定义的行为 永远不要尝试它 让我们看看当您尝试时会发生什么free 自动变量 堆管理器必须推断出如何获取内存块的所有权 为此 它要么必须使
  • 将代码拆分为标头/源文件

    我从 Asio 的示例页面中获取了以下代码 class tcp connection public boost enable shared from this
  • 是否可以有一个 out ParameterExpression?

    我想定义一个 Lambda 表达式out范围 有可能做到吗 下面是我尝试过的 C Net 4 0 控制台应用程序的代码片段 正如您在 procedure25 中看到的 我可以使用 lambda 表达式来定义具有输出参数的委托 但是 当我想使
  • 耐用功能是否适合大量活动?

    我有一个场景 需要计算 500k 活动 都是小算盘 由于限制 我只能同时计算 30 个 想象一下下面的简单示例 FunctionName Crawl public static async Task
  • 当前的 x86 架构是否支持非临时加载(来自“正常”内存)?

    我知道有关此主题的多个问题 但是 我没有看到任何明确的答案或任何基准测量 因此 我创建了一个处理两个整数数组的简单程序 第一个数组a非常大 64 MB 第二个数组b很小 无法放入 L1 缓存 程序迭代a并将其元素添加到相应的元素中b在模块化
  • WinRT 定时注销

    我正在开发一个 WinRT 应用程序 要求之一是应用程序应具有 定时注销 功能 这意味着在任何屏幕上 如果应用程序空闲了 10 分钟 应用程序应该注销并导航回主屏幕 显然 执行此操作的强力方法是在每个页面的每个网格上连接指针按下事件 并在触
  • 使用 CSharpCodeProvider 类编译 C# 7.3 的 C# 编译器版本是什么?

    我想使用 Microsoft CSharp CSharpCodeProvider 类来编译 C 7 3 代码 编译器版本在 IDictionary 中指定 在创建新的 CSharpCodeProvider 时将其作为输入 例如 Compil

随机推荐

  • 广工人福利,openwrt+gduth3c通过inode认证,妈妈再也不用担心我要用电脑开wifi了

    刚开校园网的时候 天天都只能用电脑开wifi 用类似于360wifi 猎豹wifi之类的软件要经常开着电脑 而且电脑网卡发射功率又小 上个厕所wifi就断了 睡觉前在床上还没wifi用 超级不爽 于是从家里面拿来了放在自己房间挂迅雷百度云的
  • x86下的C函数调用惯例

    1 从汇编到C 1 1 汇编语言的局限性 汇编语言是一种符号化了的机器语言 machine code 即用指令助记符 符号地址 标号等符号书写程序的语言 汇编语句与机器语句一一对应 它只是把每条指令及数据用便于记忆的符号书写而已 汇编语言
  • 用自己的数据增量训练预训练语言模型

    预训练模型给各类NLP任务的性能带来了巨大的提升 预训练模型通常是在通用领域的大规模文本上进行训练的 而很多场景下 使用预训练语言模型的下游任务是某些特定场景 如金融 法律等 这是如果可以用这些垂直领域的语料继续训练原始的预训练模型 对于下
  • spring配置文件解读——applicationContext.xml

    spring的配置文件 applicationContext xml 听着晴天看星晴的博客 CSDN博客
  • binutils internal struct

    http fossies org dox binutils 2 23 2 structelf internal sym html dl iterate phdr REPAIR RAX inline hook
  • [动态规划] leetcode 416. 分割等和子集

    问题描述 分割等和子集 给你一个只包含正整数的非空数组 nums 请你判断是否可以将这个数组分割成两个子集 使得两个子集的元素和相等 例子 输入nums 1 5 11 5 输出true 动态规划求解 这是一个0 1背包问题的变种 也就是每种
  • Idea工具使用经典总结

    安装教程 下载地址 https www jetbrains com idea download section windows 准备idea ideaIU 2017 2 3 exe 软件与激活包 JetbrainsCrack 2 6 9 r
  • 设备节点如何与设备驱动关联

    1 上层应用如何调用设备驱动 1 在linux中一切皆是文件 设备驱动程序对上层应用程序来说和普通文件没什么差异 2 上层应用程序通过设备节点来访问驱动程序 在驱动程序注册到内核后 用申请到的主次设备号来创建设备节点 2 向内核注册字符驱动
  • 【已解决】Error: Unable to access jarfile .\xxxx.jar

    报错类型 Error Unable to access jarfile xxxx jar 复现工具的时候 通过命令 java jar xxxx jar 运行 jar 包报了这个错误 报错原因是 在命令行中出现的路径下找不到 xxxx jar
  • micro-app在vue-element-admin中一些使用研究

    1 简述 本文承接上一篇micro app在vue element admi中的搭建 对micro app在vue element admin中的一些平时开发中常用的功能做了一些研究 本文代码 2 路由 关于路由 这边从两方面进行研究 一方
  • ass字幕格式

    ssa ass字幕格式全解析 内容 一 概述 二 文件各个部分解析 三 各种类型的行 四 Script Info 部分的标题行 五 v4 Styles 部分的风格行Style 六 Events 事件部分的对话行Dialogue 七 Even
  • 高通功耗调试18之Tsensor中断频繁触发导致低温下待机功耗高的问题

    问题背景 在内核4 9及之后的版本 低于5C环境温度下待机 由于触发了Tsensor的低温保护机制 可能会遇到较频繁 的tsens中断 11 22 07 12 27 914969 0 0 W GICv3 gic show resume ir
  • [echarts]柱状图的点击事件

    先来一段简洁的写echarts图表的代码 这样获取echarts的dom节点是因为 如果将柱状图封装成了一个组件 在一个页面中多次使用 若还是按常规获取dom节点 会报一个警告 let charts echarts getInstanceB
  • Linux驱动开发(应用程序如何调用驱动)

    1 添加读写接口 1 在应用代码中 2 在驱动代码中 2 应用和驱动之间的数据交换 1 copy from user 用来将数据从用户空间复制到内核空间 2 copy to user 用来将数据从内核空间复制到用户空间 3 write和re
  • DAPM之一:概述

    DAPM Dynamic Audio Power Management 对应结构体是snd soc dapm widget和snd soc dapm route 对应的操作函数是snd soc dapm new controls snd s
  • C++对象模型和this指针

    C 对象模型和this指针 成员变量和成员函数分开存储 在C 中 类内的成员变量和成员函数分开存储 只有非静态成员变量才属于类的对象上 include
  • 逐梦C++补遗篇之一:cout与cerr的区分

    逐梦C 补遗篇之一 cout与cerr的区分 1 从定义看区别 cout 标准输出流 带缓冲 默认输出目的地为屏幕 可以被重定向 cerr 标准错误输出 不带缓冲 输出目的地为屏幕 一般不被重定向 缓冲 带缓冲 就是系统会为你分配一个缓冲区
  • 精彩观点一览

    7月20日下午 大模型的发展路径论坛于北京成功举办 大模型的发展路径论坛作为2023中国互联网大会的分论坛之一 由中国互联网协会人工智能工作委员会承办 中国信通院云计算与大数据研究所 华为云大数据与AI业务协办 并得到阿里云 北京智源研究院
  • 理解什么是 JMM

    理解什么是 JMM 本文已收录至 GitHub https github com yifanzheng java notes Java 虚拟机是一个完整的计算机的一个模型 因此这个模型自然也包含一个内存模型 Java 内存模型 也就是说 J
  • 最短路算法——Dijkstra

    Dijkstra 在大多数最短路径问题中 Dijkstra 算法是最常用 效率最高的 它是一种 单源 最短路径算法 一次计算能得到从一个起点 s 到其他所有点的最短距离长度 最短路径的途径点 一 Dijkstra的算法思想 Dijkstra