Dijkstra算法入门
来源:互联网 发布:普法网络知识竞赛答题 编辑:程序博客网 时间:2024/05/29 17:26
0. 前言
最短路径问题(Shortest Path Problem)是一类非常重要的问题,它出现在很多应用领域,例如车辆导航、路由选择、机器人运动规划、物流配送等。Dijkstra 算法是一种解决最短路径问题的经典算法,同时也是计算机科学中最有名的算法之一。其方法简洁,但蕴藏的思想却很深刻。通过学习 Dijkstra 算法,既可以掌握分析、解决问题的方法,也可以作为进一步学习其它搜索算法的基础。用一句时髦的话说,Dijkstra 算法——你值得拥有。
可是对于缺少一定基础知识的初学者,彻底弄懂 Dijkstra 算法的原理有些困难。现有的讲解 Dijkstra 算法的书籍和博客不求甚解,只会照本宣科地告诉初学者算法的流程,而忽视了算法的由来和内在的逻辑。初学者读完后仍然似懂非懂,知其然而不知其所以然。而且初学者在编程实现时又会遇到不少麻烦,让他们举步维艰。本文的目的是帮助初学者尽快入门,为此在表达上力求通俗易懂。文中出现的程序都提供了源代码(Mathematica),方便初学者体验程序的运行过程,并对其解剖研究。
1. 最短路径问题
最短路径问题的研究范围很大,我们只讨论最简单的情况,即:告诉你一个起点和一个目标点,找到从起点出发到达目标点的最短路径,这又称为单源单目标最短路径问题。一般来说,找到一条连接起点和目标点的路径并不太难,但是想找到最短的路径可就没那么容易了。有时,寻找最短路径实在太难了,人们不得不放弃,转而寻找一条差不多短的路径。
在解决这个问题之前,我们首先需要对问题进行描述。现实世界总是存在各种约束,比如汽车应该沿着道路行驶(你不能把车开到墙里)、电流必须在电缆上传输、上网产生的数据包只能在路由器之间的网线传递。如果不存在约束,那么最短问题就没有研究价值了——只需要在起点和目标点之间画直线就行了。为了表示现实中的各种约束,同时也为了便于用数学方法进行处理,通常选择数学中的“图”(graph)进行描述(研究“图”的数学学科称为“图论”,图 1(a) 展示了一个“图”的例子)。“图”由两种东西组成:节点(vertex)和 边(edge)。图 1(a) 中的圆点表示节点,黑色线段表示边。我们一般用小写字母表示节点,例如节点
既然寻找最短路径是件很难的事,我们最好先从简单的情况入手。考虑如图 1(a) 所示的例子,这个“图”的节点排列成一个规则网格,所有边的长度都相等,假设长度都是1吧。图中也标出了起点(红色点)和目标点 (绿色点),你能找到它们之间的最短路径吗?
答案揭晓,最短路径就是图 1(b) 中的黄色线段(为了突出它,我特意画得比普通的边粗一些)。因为两点间直线段最短,起点和目标点之间刚好存在这样组成直线段的边。你可能觉得这太简单了,甚至有智商被侮辱的感觉。事实恰恰相反,这个例子不是太肤浅了,而是太深刻了。我们可以从中找到一条规律,这条规律太重要了,以至于我不得不将它单独放在一段:
规律
数学家们喜欢干的一件事就是推广——将特殊推广到一般,将简单推广到复杂。比如牛顿的第一个数学发现就是将二项展开式的系数从正整数推广到负数和分数。我们也来试着将前面这条规律推广一下,这样就得到了下一条规律:
规律
想想看,我们能不能将规律1换种说法:一条最短路径上的任意两个节点之间的最短路径仍然在这条路径上。看起来好像差不多,但其实是不严谨的,因为我们并不知道最短路径是不是唯一的。如果任意两个节点之间的最短路径都只有一条,那么这样说就是对的。但是在有些情况下,两个节点之间的最短路径可能会有不止一条 (它们的长度都是最短的,但经过的节点不同)。所以我们还是应该采用规律1的说法。
2. 搜索算法
2.1 松弛 (relax)
啊哈!我们的小世界开始有意思起来了,我们发现了其中的一条规律。但别高兴的太早,我们怎么利用这条规律呢?如果我给你一条路径,你可以用规律 1 来验证它到底是不是最短的。如果你能在这条路径上找到两个节点,在它们之间有更短的路径,那你可以自信地说我给你的路径肯定不是最短的。注意:规律 1 的重点是“最短路径上”。非最短路径上也可能包含最短的子路径;而两个最短路径拼接到一起得到的路径未必是最短的。规律 1 没有告诉我们怎么计算最短路径。我们试试把规律 1 反过来是什么,这样就得到了另一条规律:
规律
我们同样不知道规律 2 是不是成立。但经验告诉我们,它很有可能是对的。我们也需要从逻辑上检验规律 2 的正确性。这里我们可以投机取巧,既然规律 2 适用于路径上的任意两个节点,我们不妨选择这条路径的起点和目标点。因为起点和目标点间的最短路径与这条路径重合,显然这条路径就是最短路径。所以规律 2 是正确的。太棒了,因为我们在小世界中又发现了一个新规律。与规律 1 不同的是,规律 2 的提供了一种操作 —— 把一条不是最短的路径变成最短路径的操作:
1. 随便选择一条连接起点和目标点的路径(不一定最短)。
2. 在这条路径上任意选择两个节点,搜索它们之间的最短路径。
3. 如果找到的最短路径不在原路径上,就用最短路径替换掉原来路径的那部分。
4. 重复第2步和第3步,直到这条路径的长度不再改变。
我们同样不知道规律 2 是不是成立。但经验告诉我们,它很有可能是对的。我们也需要从逻辑上检验规律 2 的正确性。这里我们可以投机取巧,既然规律 2 适用于路径上的任意两个节点,我们不妨选择这条路径的起点和目标点。因为起点和目标点间的最短路径与这条路径重合,显然这条路径就是最短路径。所以规律 2 是正确的。太棒了,因为我们在小世界中又发现了一个新规律。与规律 1 不同的是,规律 2 的提供了一种操作——把一条不是最短的路径变成最短路径的操作:
依照上面几步操作我们最终总能找到最短路径。可这是一个好方法吗?看起来似乎不太好。首先我们并不知道运行多少步才能找到短路径。假如你迷路了,向别人问路。那人给你指了一个方向却没告诉你还有多远,你会不会心里没底。其次是第 2 步,很明显第 2 步本身就是一个最短路径问题,它如何求解我们还是不知道。
虽然上述方法缺少实用价值,但至少它的方向是对的,我们可以从中受到启发。这个方法可以形象的比作被抻长的橡皮筋恢复的过程。如果将路径视为橡皮筋,那么路径的长度就对应橡皮筋中储存的弹性势能。最短路径就是自然状态下(不受外力)的橡皮筋,它不会再缩短了。开始随意确定的路径相当于被抻长的橡皮筋,而以后每一次超近道都可以看成橡皮筋在自身弹力作用下缩短恢复的过程。我们称这一过程为“松弛”(relax),意思就是松开抻长的橡皮筋,让它缩短从而释放掉多余的弹性势能,如图 5 所示。
松弛的过程很简单,用程序实现也不复杂。为了便于理解,我把松弛程序用伪代码写出来,如 Algorithm 1 所示。Relax 函数负责实现松弛,它的输入是两个相邻的节点
2.2 所有边依次松弛
我们知道了只松弛一部分边达不到理想的效果,原因就是初始路径不一定与最短路径有一样的节点。当然,我们不知道最短路径经过哪些的节点。能否扩大范围,对所有的边都松弛呢?当然可以。只是除了起点之外,我们对其它所有节点的值都不清楚,这意味着我们无法判断松弛的条件。不过,我们可以认为起点的值是0,因为起点到起点的路径最短就是0,不会有比0更短的路径了。这时我们不再需要先寻找一个初始路径了。我们可以将其它所有节点的值都认为是无穷大,也就是说没有路径到达它们。每应用一次松弛,它们的值都会改变一点。我们可以编程实现这个过程,如 Algorithm 2 所示。
下面我们详细解释 Algorithm 2 的每一步。
1. 首先算法进行初始化,也就是我们刚刚讨论过的,设置节点的值和母节点。由于计算机没办法表示无穷大,把初始值设置成一个很大的数就行 (比如 1000,实际上只要大于所有可能路径的最大值就可以)。
2. 第一个 for 循环执行
3. 第二个 for 循环负责扫描边,它从“图”的所有边的集合
4. 第三步的 if 语句用于判断是否需要松弛,如果
我们用该程序求解图 3(a) 所示的例子,看看能得到什么结果(代码可见文件 Example 1.nb)。这个程序只改变节点的值和母节点。可是节点值只是一堆数字,为了更直观地展示结果,我将每个节点的值用等比例高的小球表示,如图 7(a) 所示。值越大,小球的位置越高、颜色越暖(偏红色),反之越小就越爱、颜色越冷(偏蓝色)。从图中可以看出,第一次扫描后起点附近的节点值变化较大,但是远处的节点值仍为初始设定的值,并没有怎么变化。我们增加
这说明所有节点的值都稳定到了一个固定值,同时也意味着稳定后的值不存在满足松弛条件的边了。因为如果存在的话, 一定有节点的值会减少(这又是由于松弛条件的标准是严格小于
我们不仅得到了起点到目标点的最短路径,还顺便把起点到所有节点的最短路径都找出来了。问题解决了,到了说再见的时候了吗?如果你对这个计算结果还满意的话,那么确实可以结束了。但如果你是个完美主义者,这个方法还值得进一步雕琢。在大型的“图”中,例如有
2.3 标记法 (Labeling method)
我们把上一节采用的方法称为“所有边依次松弛方法”。我们为什么要强调其中的“依次”呢?因为程序是按照边定义的顺序(也就是在集合
所以我们需要特别注意值发生变化的那些节点,只有它们的邻居才会松弛。为了利用这一信息,我们将节点分为两类:值发生变化的节点和值没变化的节点。为了区分这两种节点,我们给每个节点一个标签(label)。给那些值发生变化的节点发一个 changed 标签,而给那些值没变化的节点发一个 unchanged 标签。下面我们给出“所有边依次松弛”方法的改进,这就是“标记法”(labeling method)。按照命名的规则,名字应该体现事物的本质特征,这里我们使用“标记”,原因就在于这是它区别于前辈的主要特点。标记法的伪代码如 Algorithm 4 所示。与它的前辈不同的是,我们不再需要人工试探如何选择循环次数
1. 首先同样是初始化,这次我们多了一步 —– 定义
2. while 循环依次从
3. for 循环依次取出
4. if 判断语句我们已经见过了,它仍然负责判断松弛条件。不过这次我们判断
5. for 循环结束后,
我们前进了一大步,这值得庆祝一下!不过我们还可以再接再厉。标记法仍给我们预留了改进的空间:比如第 3 步中“依次取出
下面这个例子也许能给我们一些启示 (代码可见文件 Example 2.nb)。图 9(a) 展示了一颗“树形”图,我们只需要关注树根和树干部分即可。这部分非常简单,由 4 个节点组成 —— 起点
下面我们使用标记法求最短路径。首先进行初始化,起点
1. 如果是按照
2.4 改进的标记法 (Modified labeling method)
图9(a)所示的例子给了我们一个启示,那就是在访问邻居时应该遵守一定的规则——应该先去敲值最小的邻居的门。让我们的思维稍微跳跃一下,既然访问邻居要按照最小原则,那么从
2.5 Dijkstra方法
Everything should be made as simple as possible, but not simpler .
—— Albert Einstein
我们回过头来看看改进的标记法(Algorithm 5)。即便你是一个完美主义者,你也不得不承认,它已经相当简洁了。短短十行代码就能解决看似复杂的最短路径问题。爱因斯坦说过:任何事情都应该越简单越好,而不是更简单。这句话怎么理解呢?我觉得,对于我们正在解决的最短路径问题来说,追求“越简单越好”就是尽量去除算法中多余的东西,这样我们的算法才能轻装上阵,执行效率才会更高。从这个角度看,“简单”是个优点。可是物极必反,如果我们过分追求简单(总想着“更简单”),把简单(而不是算法的执行效率)当成我们唯一的目的,那么我们就钻进了牛角尖,违背了我们的初衷 —— 设计更好更快的算法。我丝毫不怀疑你能写出更简单的算法,但是在追求简单和运算效率二者之间,请保持平衡。
Dijkstra 是平衡的大师。以他的名字命名的 Dijkstra 方法在不牺牲执行效率的前提下,比我们的改进标记法更加简单。Dijkstra 方法 (如 Algorithm 6 所示)只需要一个列表
虽然 Dijkstra 方法很简单,但是从代码的字里行间,我们看不出来它为什么能找到最短路径。下面我们从逻辑上分析一下:
在程序运行之前,所有的节点都是未访问节点(即
为了证明 Dijkstra 方法确实能找到最短路径,我们只需要证明被踢出节点的值就是它的最小值。在证明之前,先定义一个概念。我们将从起点
下面的证明采用了数学归纳法,这需要两步证明:
第一步证明命题在第 1 个节点的情况下成立。这很容易,因为起点的值最小,所以第一个被从
第二步证明如果命题在前
第二步的证明: 根据 Dijkstra 方法的规则,值最小的节点最先被踢出来。所以节点
1.
2.
3. 排除了以上两种极端的情况,唯一剩下的就是
先看第一种情况,如果
再看第二种情况,如果
综上所述,这两种情况都不成立,所以目前找到的这条路径就是
这样我们就证明了,每个被踢出去的节点
证明 Dijkstra 方法花了我们不少力气。你可能会好奇——Dijkstra 到底是怎么想出这个方法的。下面我们来了解一下背景。
2.6 Dijkstra和他的算法
Edsger Wybe Dijkstra 的父亲是高中化学老师,母亲是业余数学家。1956 年从莱顿大学数学和理论物理专业毕业后,Dijkstra 到阿姆斯特丹大学攻读博士,3 年后毕业。毕业论文题目是:自动计算机的通信方式,研究内容是第一代商业计算机的汇编语言设计。Dijkstra 终生过着斯巴达式的简朴生活
1956 年,Dijkstra 在阿姆斯特丹数学中心工作时被指派了一项任务:为演示新建造的计算机而设计一个问题并编写对应的求解程序。问题要能够展示计算机的性能,同时越简单越好,以便于被更多的人理解。Dijkstra 为此挑选了最短路径问题:在荷兰的 64 个城市之间寻找最短的运输路线。随后他开始思考求解算法。一次在咖啡馆里与未婚妻消遣时,Dijkstra 花了20分钟构思出了这个问题的解决方法,期间没有使用演算纸和笔,Dijkstra 算法由此诞生。三年后,Dijkstra 将这一方法连同对另一个类似问题的解答撰写成论文并发表在学术期刊上,题目是 A Note on Two Problems in Connexion with Graphs。论文只有两页半长,其中没有一个数学公式,没有一幅图,甚至连一个例子也没有。迄今为止,这篇文章已经被引用超过 17000 次。(事实上,Dijkstra 并不是 Dijkstra 算法的最早发现者,其思想至少在 1950 年代早期就出现了,只不过在当时只流传于几个小圈子里)
在论文中,Dijkstra 介绍了最短路径问题后,紧接着写下了一句耐人寻味的话。然后,他简单描述了算法运行的过程和实现的细节。我们要是想知道 Dijkstra 是如何灵感迸发,从而构思出这个优雅的算法的,就只能仔细读读这句话了。这句话如下:
“We use the fact that, if
R is a node on the minimal path fromP toQ , knowledge of the latter implies the knowledge of the minimal path fromP toR . ”我们借助这样一个事实:如果
R 是从P 到Q 的最短路径上的一个节点,那么知道了后者(P 到Q 的最短路径)就等于知道了P 到R 的最短路径。
这句话也许让你觉得似曾相识。没错,那就是我们最开始得到的规律1。只不过,Dijkstra 将目光放在了起点
规律1有一个正式的名字 —— 最优性原理,它的正式提出者 —— Richard Bellman 是这样说的:
“An optimal policy has the property that whatever the initial state and initial decision are, the remaining decisions must constitute an optimal policy with regard to the state resulting from the first decision.”
任何一个最优策略都有这样的性质:不管初始状态和初始决策是什么,随后的决策相对于初次决策之后的状态必然构成最优策略。
Bellman 将目光放在了任意节点到目标点上 (换句话说,从后向前推)。我们得到的规律1只是“最优性原理”在最短路径问题上的一个特例。最优性原理也是一类数学方法 —— 动态规划(Dynamic Programming)的理论基础。Bellman 早在1940年代就开始了动态规划的研究,1950~1954 年一系列的论文和报告标志着动态规划理论的成熟。我们无从得知 Dijkstra 是否了解 Bellman 的工作或者从中受到启发,因为他在文中并没有提到 Bellman 的工作,而只引用了 Ford 的报告。当时 Ford 和 Bellman 是同事,二人共同就职于大名鼎鼎的兰德公司(RAND)
到此为止,关于 Dijkstra 算法我们就告一段落了。你可能会好奇,Dijkstra 算法还有改进的余地吗?我觉得,Dijkstra 算法已经是较“原始”的算法了,它的适用范围和性能已经满足不了今天的需求了。对它的改进一直在进行中,新的算法层出不穷(
代码下载地址:http://pan.baidu.com/s/1dFp4bxJ
[1] R A Krzysztof, 2002, Edsger Wybe Dijkstra (1930–2002): A Portrait of a Genius. Formal Aspects of Computing, 14:92–98.
[2] M Sniedovich, 2006, Dijkstra’s Algorithm Revisited: the Dynamic Programming Connexion. Control and Cybernetics, 35(3):599–620.
- Dijkstra算法入门
- dijkstra算法入门
- 【codevs1557】 热浪, Dijkstra算法入门
- Dijkstra入门
- Dijkstra算法
- dijkstra算法
- Dijkstra算法
- Dijkstra算法
- Dijkstra算法
- Dijkstra算法
- Dijkstra 算法
- Dijkstra算法
- Dijkstra算法
- Dijkstra算法
- Dijkstra 算法
- Dijkstra 算法
- dijkstra算法
- Dijkstra 算法
- |算法讨论|贪心算法 学习笔记
- python to exe pyinstaller
- 微信远程访问电脑资源-基于Itchat
- T-20
- windows环境下安装magento
- Dijkstra算法入门
- 1.13 12 分数求和
- 类与对象
- lua split函数
- [codevs1766]装果子
- 为 Java EE 应用提供的 9 种 Docker 方法
- 可视区域内鼠标拖拽框
- 数据结构和算法--二叉树学习
- linux笔记之信号量