Learn Dijkstra For The Last Time

news/2024/5/20 15:12:00/文章来源:https://www.cnblogs.com/Clouder-Blog/p/learn-dijkstra.html

博客链接:https://www.codein.icu/learn-dijkstra/

Introduction

Dijkstra 算法是用于求解非负权图单源最短路的经典算法。

市面上的大部分教程都仅仅停留在「如何实现 Dijkstra 算法」的层面。从应用角度,这当然无可厚非。但理解算法本身,也不失为一件乐事。

问自己这样几个问题:

  • Dijkstra 算法的每个过程是在干什么?
  • Dijkstra 算法为什么是正确的?

也许你在小学就已经能熟练的打出 Dijkstra 的板子,拿它在各大 OJ 上厮杀。
也许你曾经随便找一篇博文,花费 10min 把代码敲熟便「学会了」Dijkstra 算法。
That is not true.

在我给小 OIer 们准备上最短路课程时,我才真正意识到,其实我从未理解过 Dijkstra 算法。我可以手指不停地将它敲出来,也会记录最短路径、最短路计数之类的拓展,但我不明白它的 Key Inspiration 是什么,不理解它「为什么」这么做,「为什么」是正确的。

我振臂高呼——「Tell Me Why!!!

于是我花费了许多时间,在网上查找相关资料,但要么只能查到诘屈聱牙的严谨理论证明,要么就是 csdn 之流的垃圾场上千篇一律的不求甚解

最后,我在 Stack Overflow 上找到了一篇回答,用一个形象的比喻解决了我的困惑。以此为立足点,刷新了我对 Dijkstra 的理解。也许这算是一种「Aha! moment」.

这便是本文的由来。

Analogy

Start With BFS

我们曾经学过 BFS(Breath-First Search,宽度优先搜索),如果没有学过的话建议先学习一下。

BFS 同样可以求解最短路,不过它有着更强的限制条件——「边的距离相等」,或者说,「边权为 \(1\)」.

在 BFS 的过程中,每个节点「首次」被访问,即为最短路。

这个是很好理解的,因为我们第一轮 BFS 访问的节点距离起点距离为 \(1\),第二轮距离为 \(2\),以此类推,首次访问某节点,就一定通过了最短的路径。

用一个形象的比喻:每一条边就像是一根 \(1m\) 长的管道,从起点开始注水,水流速度为 \(1m/s\),每个结点首次被水浸泡时,对应的是时间 \(t\) 最小,而 \(s = vt\),那么路程也最小,即最短路。

Water Filling

再回到我们的 Dijkstra 算法上。

相比于 BFS 的图,我们现在的图有何不同呢?

  • 边权可以 \(>1\)
  • 边权可以为 \(0\)

第二条暂且不谈,因为 BFS 事实上是可以处理边权为 0/1 的图的,只需要使用双端队列,将边权为 0 的边拓展的点插入队头即可。这个叫做 0-1 BFS.

我们可以沿用 BFS 的思路求最短路。可以将这张图转化为 BFS 能处理的图。

拆边建虚点。
比如说,有一条边权为 \(2\) 的边,我们把它拆成两条边权为 \(1\) 的边,以此类推,不就能将图转化为 BFS 可处理的图了吗?

imageimage

这样一来,我们就得到了一个复杂度为 \(O(\sum w)\) 的算法。由于边权可以很大,这个算法的复杂度是不可接受的。

可以怎么样优化一下呢?其实,每次只走 \(1m\) 是非常无效的行为。可以想想办法,让我们能一次走更长的距离。

image

比如在这张图上,从 1 号结点出发,我们开始拓展:

image

image

我们真正关注的,事实上是「实际存在的点」,而中间的步骤是无关紧要的。

比如上面这个过程,经过了两步,才抵达了 3 号结点。那能不能找到一种方法,一次性走过很多步而直接抵达 3 号结点呢?

其实是可以的。

Solution

我们将所有的、已经被水浸泡的点的集合,记做集合 \(\mathbf{S}\).

根据我们先前的描述,可以知道,集合 \(\mathbf{S}\) 中的点,都已经求出了最短路距离 \(dis\).

水将从集合 \(\mathbf{S}\) 中的点,经过管道继续流向其他点。下一个被浸泡的点,一定是已被浸泡的点的相邻点。

我们将与集合 \(\mathbf{S}\) 点相邻的所有点的集合记为集合 \(\mathbf{T}\).

  • 集合 \(\mathbf{T}\) 中的点未被浸泡。
  • 每个点首次被浸泡时,水流一定走的是最短路。对应的距离即为最短路距离。
  • 下一个被浸泡的点一定从集合 \(\mathbf{T}\) 中产生。

现在,我们要做的就是,在集合 \(\mathbf{T}\) 中找到下一个被浸泡的点。

我们将某个点加入集合 \(\mathbf{S}\) 后,会更新起点到其相邻点的距离 \(dis\).
下一个被浸泡的点,一定是集合 \(\mathbf{T}\)\(dis\) 最小,也就是离起点最近的点。

这样,算法流程就明确了:

  • 初始,将起点加入集合 \(\mathbf{S}\),将相邻点加入集合 \(\mathbf{T}\)
  • 找出集合 \(\mathbf{T}\) 中距离起点最近的点,加入集合 \(\mathbf{S}\),将其相邻点加入集合 \(\mathbf{T}\)
  • 重复第二步,直到所有点都加入集合 \(\mathbf{S}\)

这就像是做了一个加速:我们的水本来要经过许多步才能泡到下一个节点,但我们跳过中间步骤,直接泡到下一个节点。

image
从 1 号点出发,发现浸泡到下一个节点需要走 7 步。

image

合七步为一步,接下来发现浸泡三号节点是最快的。

image

接下来,从 3 号出发浸泡 6 号节点是最快的。

image

以此类推……剩下的就不画了。

Proof

从形象到抽象。

  • 初始,将起点加入集合 \(\mathbf{S}\),将相邻点加入集合 \(\mathbf{T}\)
  • 找出集合 \(\mathbf{T}\) 中距离起点最近的点,加入集合 \(\mathbf{S}\),将其相邻点加入集合 \(\mathbf{T}\)
  • 重复第二步,直到所有点都加入集合 \(\mathbf{S}\)

定义当前情况下从起点到点 \(u\) 的最短距离为 \(\operatorname{D}(u)\),从起点到点 \(u\) 的真实最短距离为 \(\operatorname{dis}(u)\). 显然有:\(\operatorname{D}(u) \ge \operatorname{dis}(u)\).

第二步中,找出集合 \(\mathbf{T}\)\(\operatorname{D}(u)\) 最小的点 \(u\),证明该点满足 \(\operatorname{D}(u) = \operatorname{dis}(u)\).

根据定义,集合 \(\mathbf{T}\) 中的点与集合 \(\mathbf{S}\) 中的点直接相连,而 \(\operatorname{D}(u) = \operatorname{dis}(x) + w\),目前的最短路就是从 \(x\) 点经过某条边到达 \(u\) 点。
当前的 \(\operatorname{D}(u)\) 是所有从集合 \(\mathbf{S}\) 中点出发,经过一条边到达 \(u\) 点的路径的最小距离。

从起点到达 \(u\) 点的最短路径的一部分一定也是最短路径。
比如途中经过某点 \(x\),则当前路径一定也是起点到达 \(x\) 的最短路径。
否则,走起点到达 \(x\) 的最短路径,再走剩下的路径,即可得到起点到达 \(u\) 点的一条更短路,矛盾。

假设存在某一条更短路能取得 \(\operatorname{dis}(u) < \operatorname{D}(u)\),那么这条更短路,一定是从集合 \(\mathbf{S}\) 中某点出发,经过至少两条边后到达 \(u\) 点。
证明:若只经过一条边,则该更短路本来会更新 \(\operatorname{D}(u)\),使得 \(\operatorname{D}(u) = \operatorname{dis}(u)\),矛盾。

现在证明,该更短路不存在。

我们当前的最短路可以记做:\(x \to u\).
而更短路可以记做:\(y \to t \to \ldots \to u\),其中 \(\ldots\) 是任意路径,可以包含 \(0\) 及更多个点。

\(x,y \in \mathbf{S}\),那么 \(u,y \in \mathbf{T}\).
根据定义,\(\operatorname{D}(u)\) 是集合 \(\mathbf{T}\) 中最小的,那么 \(\operatorname{D}(u) \le \operatorname{D}(t)\).
那么从 \(t\) 点出发,再经过某条路径到达 \(u\) 点,记该路径长度为 \(L\),只要满足 \(L \ge 0\),就有 \(\operatorname{D}(t) + L \ge \operatorname{D}(u)\).
那么这条路径一定不能取得比当前 \(\operatorname{D}(u)\) 更优的值。
因此,该更短路不存在。

PS:这里还能看出 Dijkstra 为什么不能处理负权图。在负权图上,\(L < 0\) 会导致 \(\operatorname{D}(t) + L \ge \operatorname{D}(u)\) 这个不等式并不成立,核心的贪心思想就无效了。

此时,取出 \(\operatorname{D}(u)\) 最小的点,也未必满足 \(\operatorname{D}(u) = \operatorname{dis}(u)\).

接下来,每次第二步加入集合 \(\mathbf{S}\) 的点都满足 \(\operatorname{D}(u) = \operatorname{dis}(u)\),而初始起点显然也满足,则集合 \(\mathbf{S}\) 中的点都满足 \(\operatorname{D}(u) = \operatorname{dis}(u)\).

Implementation

描述到这里,相信大家写出代码没啥难度了。

  • 初始,将起点加入集合 \(\mathbf{S}\),将相邻点加入集合 \(\mathbf{T}\)
  • 找出集合 \(\mathbf{T}\) 中距离起点最近的点,加入集合 \(\mathbf{S}\),将其相邻点加入集合 \(\mathbf{T}\)
  • 重复第二步,直到所有点都加入集合 \(\mathbf{S}\)

各种实现的区别,无非就是在第二步,找出距离起点最近的点时用了不同的方法。

节选一段 OI-Wiki 上的描述。

  • 暴力:不使用任何数据结构进行维护,每次 2 操作执行完毕后,直接在集合中暴力寻找最短路长度最小的结点。2 操作总时间复杂度为 \(O(m)\),1 操作总时间复杂度为 \(O(n^2)\),全过程的时间复杂度为 \(O(n^2 + m) = O(n^2)\)
  • 二叉堆:每成功松弛一条边 \((u,v)\),就将 \(v\) 插入二叉堆中(如果 \(v\) 已经在二叉堆中,直接修改相应元素的权值即可),1 操作直接取堆顶结点即可。共计 \(O(m)\) 次二叉堆上的插入(修改)操作,\(O(n)\) 次删除堆顶操作,而插入(修改)和删除的时间复杂度均为 \(O(\log n)\),时间复杂度为 \(O((n+m) \log n) = O(m\log n)\)
  • 优先队列:和二叉堆类似,但使用优先队列时,如果同一个点的最短路被更新多次,因为先前更新时插入的元素不能被删除,也不能被修改,只能留在优先队列中,故优先队列内的元素个数是 \(O(m)\) 的,时间复杂度为 \(O(m \log m)\)
  • Fibonacci 堆:和前面二者类似,但 Fibonacci 堆插入的时间复杂度为 \(O(1)\),故时间复杂度为 \(O(n\log n + m) = O(n \log n)\),时间复杂度最优。但因为 Fibonacci 堆较二叉堆不易实现,效率优势也不够大 [1](https://oi-wiki.org/graph/shortest-path/#fn:1) ,算法竞赛中较少使用。
  • 线段树:和二叉堆原理类似,不过将每次成功松弛后插入二叉堆的操作改为在线段树上执行单点修改,而 1 操作则是线段树上的全局查询最小值。时间复杂度为 \(O(m \log n)\)

代码实现提供 OI-Wiki 上的 C++ 版本。


// C++ Version
struct edge {int v, w;
};struct node {int dis, u;bool operator>(const node& a) const { return dis > a.dis; }
};vector<edge> e[maxn];
int dis[maxn], vis[maxn];
priority_queue<node, vector<node>, greater<node> > q;void dijkstra(int n, int s) {memset(dis, 63, sizeof(dis));dis[s] = 0;q.push({0, s});while (!q.empty()) {int u = q.top().u;q.pop();if (vis[u]) continue;vis[u] = 1;for (auto ed : e[u]) {int v = ed.v, w = ed.w;if (dis[v] > dis[u] + w) {dis[v] = dis[u] + w;q.push({dis[v], v});}}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_2478.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

自己de搭建博客记录

鸽子啊鸽子一去不复返自己de搭建博客记录因为奇奇怪怪的原因所以开始学着自己搭建一个博客了 但是估计搭好了也不会常更新,连博客园都咕了一个月了 先水水免得自己忘记了,要学的还有挺多 突然发现博客阅读量猛涨,看了下貌似是N2的插件文章被爬到各种奇怪网站了-1 参考资料 参…

The forked VM terminated without properly saying goodbye. VM crash or System.exit called?

1、maven构建报错: [INFO] BUILD FAILURE[INFO] ------------------------------------------------------------------------[INFO] Total time: 47.849 s[INFO] Finished at: 2022-08-19T08:05:22+08:00[INFO] ----------------------------------------------------------…

阿里云 EMAS Serverless 重磅发布

EMAS Serverless重磅发布, 为应用开发者特别是多端开发者实现一站式应用开发提供了全新的开发体验。为了更好的布道推广 Serverless 开发生态,现面向开发者/学生提供免费套餐(不是只1个月哦,你懂的),针对业务发展的不同阶段,提供多种套餐和按量付费模式,请来阿里云官网…

Windows安全加固

实验环境 操作系统:Windows Server 2012 系统密码: 安全加固项 1、用户系统 1.1 加固项名称: Administrator账户停用 加固说明: 防止 Administrator 账户被黑客爆破出密码,避免Administrator账户被黑客利用获取计算机系统权限。只有一个管理员账户时无法禁用,需要创建另一…

WPF实现一个简单自定义管道

先看效果 xaml代码 <UserControl x:Class="WPF控件测试.Control.Pipeline" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" …

获取数的全部因子 单次查询/预处理

对于单次查询,可以直接用sqrt(n)遍历。 对于多次查询,每次都遍历会遍历多个无用的数。 可以采用打表法,直接获取数据范围内的全部数据的因子。 代码如下:int N = 100010; vector<int> factor[100010]; for (int i = 1; i <= N; i++) {for(int j=i;j<=N;j+=i)fa…

漫谈测试成长之探索——缺陷分析

​ 回顾校园生活中,我们参加每一场考试后都会对错题进行分析总结并补缺补漏,以便能更好地去应对更重要的考试。回到软件系统开发中,我们记录和跟踪缺陷的目的是什么,仅仅是为了在软件系统开发过程中跟踪Bug直至修复么?应该不止于此。我们也可以对项目缺陷进行分析,分析其…

SpringBoot读取.yml配置文件最常见的两种方式-源码及其在nacos的应用

一、前言 我们在开发中会经常遇到一些可能会变的值,比如数据库的密码,一些关键链接的配置等等。 都需要我们写在配置文件中,这样可以把这些配置文件放到nacos上进行管理,修改nacos的配置,咱们发布的包就可以动态的进行更新了,不需要重新修改打包在重新发包! 咱们今天就来…

数据结构开门篇

数据结构 1、什么是数据结构 数据结构是数据组织、管理和存储格式,其使用目的是为了高效地访问和修改数据2、时间复杂度和空间复杂度 什么是时间复杂度 时间复杂度是对一个算法运行时间长短的度量,用大O表示,记作 T(n)=O(f(n))如果运行时间是常数量级,则用常数1表示 只保留…

小红书数据 小红书爬虫 小红书接口 xhs

小红书数据 小红书接口 小红书api(小红书爬虫 xhs xiaohongshu 红书) 最新小红书APP接口稳定运行,主流接口都已部署,支持并发请求! 只抓取公开数据供作学习用途,不做引流上赞上粉业务,如有侵权,联系删除,谢谢! 只提供数据支持,不交流方案! 联系加q 2126851589! Git…

记一次有意思的 SQL 实现 → 分组后取每组的第一条记录

开心一刻今天,朋友气冲冲的走到我面前朋友:我不是谈了个女朋友,谈了三个月嘛,昨天我偷看她手机,你猜她给我备注什么我:备注什么?朋友:舔狗 2 号!我一听,气就上来了,说道:走,找她去,这婆娘确实该骂,臭不要脸的朋友拉住我,劝到:哎哎,不是去骂她,是找她理论,叫…

JavaIO流

JavaIO流 一、流的概念 内存与存储设备之间传输数据的通道。例如在内存中的程序,想要读取硬盘中的文件,必须借助流;在水库中的水流入家庭中(存储设备到内存) 二、流的分类 按方向: 输入流:将存储设备中的内容读入到内存中 输出流:将内存中的内容写入到存储设备中 流入程…

Oracle 序列学习与使用总结

Oracle序列学习与使用总结 by:授客 QQ:1033553122 简述 序列是oracle提供的用于生成一系列数字的数据库对象,序列会自动生成顺序递增的序列号,可用于提供唯一的自动递增主键。序列和视图一样,并不占用实际的存储空间,只是在数据字典中保存他的定义信息。 创建序列 当创建序…

JavaScript快速入门-04-运算符

4 运算符 4.1 算术运算符 4.1.1 概述JavaScript 提供的算术运算符如下所示:类型 符号 示例加法运算符 + a+b减法运算符 - a-b乘法运算符 * a*b除法运算符 / a/b余数运算符 % a%b自增运算符 ++ ++a/a++自减运算符 -- --a/a--指数运算符 ** a**b4.1.2 加法运算符加法运算符是最常…

电脑棒没有显示器解决方案

因为我的工作电脑是一个电脑棒(见下图),没有自带的显示屏,所以只能通过远程控制。但是据说TeamViewer可能会误判为商业使用(之后就不让你免费了),于是我就打算转ToDesk。刚开始,这个东西开机能够自启动,但是就是无法连接到服务器😓 (上图这玩意儿就是电脑棒) 之后…

select总结

select总结 我的一生 第1章-废物的一生 第50章-糟糕的婴儿 第300章-莫欺少年穷第600章-莫欺中年穷 第1000章-莫欺老年穷第1100章-不详的离去第1101章-棺材板的震动 第1150章-盗墓贼的眼泪 第1200章-死者为大

MICROSOFT SQL SERVER TO POSTGRESQL MIGRATION USING PGLOADER

To continue our migration series, today’s post will focus on pgloader. Pgloader is another Open Source data migration utility for PostgreSQL from MySQL and SQL Server. Today’s demo will migrate a sample database (StackOverflow) from MS SQL Server 2019 to…

阅读笔记: Robust Vehicle Localization in Urban Environments Using Probabilistic Maps

摘要 此篇是对Map-Based Precision Vehicle Localization in Urban Environments[4]工作的改进,在精度、地图更新、对环境改变和动态障碍物的鲁棒性方面都有所提升。具体而言,环境没有被建模为一个固定的反射强度网格,而是被建模成概率网格,每个网格都独立表达为一个对反射…

交互技术之AJAX

​/* *作者:呆萌老师 *☑csdn认证讲师 *☑51cto高级讲师 *☑腾讯课堂认证讲师 *☑网易云课堂认证讲师 *☑华为开发者学堂认证讲师 *☑爱奇艺千人名师计划成员 *在这里给大家分享技术、知识和生活 *各种干货,记得关注哦! *vx:it_daimeng */ AJAX: Asynchronous Javascript A…