迪杰斯特拉最短路径算法c语言_Why shortest? 谈谈迪杰斯特拉算法

摘要:本文以“最短路径”问题为引导,介绍一种贪心算法:迪杰斯特拉(Dijkstra),在不同的“图”的条件下细致地探讨它的工作原理。

阅读本文需要的先导知识:完全不需要任何先导知识呢!不过算法描述中会用到简易的数据结构的语言。

part1:问题导出

问题1:为相应“要致富,先修路”的号召,书记老王准备给自己辖区内几个贫困村修建公路。在地图上标注出所有可能修公路的部分如下:

c557fc22933e833662a8bea6c4a34e04.png

由于经费有限,要用最少的成本修建最短的公路把各个村庄都联通。请问王书记应该选择图中哪几条线修建成公路?

问题2:八里台学院接到一个紧急通知,领导要把这个消息快速发给各个学生辅导员。可是领导只有个别几个辅导员的联系方式,每个辅导员也只有个别几个其他导员的电话号。要怎么做才能以最快速度联系到每个辅导员?

上面两个问题既有联系也有区别。相同之处在于两个问题都在寻求某种意义上的“最短路径”(用最短的公路把所有村子联通、打最少次数电话联系到每个导员)。不同之处在于:

第一:在第一个问题中,不是每个村子之间的公路长度都相等的,但是在第二个问题中每次电话耗费的时间可以认为是一样的。

第二:第一个问题里,两个村子之间的公路是“没有确定方向”的。但是在第二个问题里,导员1有导员2的电话,不代表导员2有导员1的电话。

归纳以上两个问题,我们给出一种普遍适用的“数据结构”来描述“最短路径问题”:

定义1:(有向图)设

为图,它有节点和边两种结构。节点由边连接在一起;每条边都有固定的方向和权重,这样的图称为有向图。如果每条边的权重都是非负的,称之为非负有向图;如果每条边的权重都是正的,称之为正有向图。把权重和图本身合在一起记作

定义2:(最短路径问题)对于非负有向图

,从一个节点
出发,将所有的节点都联通,使得所需代价
最小。应当如何连接这些节点?

注意,定义中我们明确地说明了“非负”条件。这个条件是不可或缺的:Dijkstra算法在图上特定位置有负数权值的情形下会出现问题。这个问题放在后面讨论。

part 2 算法描述

我们先给出算法的伪代码,再进行解释。

在计算机中图的储存结构一般是邻接链表(Adjacency list),也就是一组线性链表,元素构成为节点名、权值以及指针。比如以下图为例,第一行第一个元素代表图中的节点A,之后的元素代表A指向的元素。

36a86ce719cd25de413632c20c152960.png

为执行这个算法,我们需要对图

(W为权重属性)上的节点初始化:我们赋予每个节点
(vertex)一个属性
,记为
。初始状态下,对图
和初始节点
定义函数
如下:
def INITIALIZE(G,s):
    for each vertex v in G.V:
        v.d = inf
        v.p = null
    s.d = 0

也就是说将每个d属性都赋值为无穷,把每个p属性都赋值为空指针,再把起始节点的d属性赋值为0。

再定义一个在迭代过程中的更新函数

,执行效果如下图:
def relax((u,v,w))
     if u.d + w(u,v) > v.d:
         v.d = u.d + w(u,v)
         v.p = u

6e7b68853298ed62a21ec76600df5a79.png
两种可能出现的情况

下面是伪代码的主体:

def Dijkstra(G, s):
    INITIALIZE(G,s)
    S = empty
    Q = G.V
    while Q is not empty:
        u = extractmin(Q)
        S = S.add(u)
        for each vertex v in G.Adj(u):
            relax(G.(u,v,w))

这个过程的实质是什么呢?粗略地想象,找出图上的最短路径,当然是要从初始节点开始,以某种方式尝试“探测”整个地图。随着探测的范围越来越大,我们可以发现越来越多的“捷径”。迪杰斯特拉算法的想法也不过如此,只是该算法的“探测方式”非常程式化、非常有效罢了。

我们可以想象Q集合代表那些“没被探测到”的元素。随着Q中元素越来越多地进入S集合,图上的节点被探测到的也越来越多。首先观察relax函数:它负责从一个“根节点”出发,试着降低一个“目标节点”的d值。由此看来,一个节点的d值可以解释为”按照目前探测到的部分得出的最短路径长度“,因为它随着探测过程不断地降低。那么p值的意义是什么呢?显然,最短路径总长变短,是因为我们选取了另一条捷径。p值便是在新的捷径上当前节点的前节点(predecessor),它包含的是具体路径的信息。

所以算法过程可以描述为:

  1. 初始化图,创建未探测列表(堆)Q和探测集S
  2. 进行循环:找出当前d值最小的节点,加入探测集S,更新所有被当前节点指向的节点d值
  3. 直到所有节点均加入S,结束循环

一个示例如下:

43dc34e968db5165a510b391e504267f.png

每个节点的最短路径,就是从该节点出发,按照p值回溯过去最终到达初始节点的路径。

part 3 算法的思想和证明

这个部分是本文的中心所在。我们从一些引理(其实这些引理就是最短路径算法的本质思想)开始,证明算法的同时讨论“为什么会想到这样的办法”。

先宏观地考虑一个简单的状态(其实这个状态可能对于计算机专业的童鞋已经够用了):所有的权值都是正的。

从初始节点s开始,如何找到一条“局部的”(即只考虑初始结点和各个邻接节点的)最短路径呢?显然,选取权值最小的那一条便是,记为

。那么下一条最短路径该如何找呢?可想而知有两种情形:
或者
,即有可能过
也可能不过。而此时的S集合恰好包含了
两点(S集合包含的节点就是那些已经找到最短路径的点)。由此猜想:

当我们在第m步确定了m条最短路径后,第m+1条的除去末尾的所有节点都应该在S中。这样一来,我们只需要找离S集合最“近”的那个节点作为下一条最短路径的末尾就可以了。而离S最近的点,恰好就指的是Q中d值最小的点!这样我们就得出迪杰斯特拉算法的雏形了。

这个猜想是否正确呢?我们可以基于“非负假设”证明。反设在m+1步找到的最短路径

中间有节点a不在S中。则说明有一条最短路径
以a为末尾。但是由于我们找最短路径的过程是严格递增的,也就是说,我们在第m+1步找到的最短路径长度一定比前m步都长,
(仔细想想为什么?),所以a已经被放在S中了。矛盾。#

下面我们讨论迪杰斯特拉算法的一般情形。实际上,该算法在有0权值的边时仍然成立,在某些情形下,甚至可以应用于负权值的图!如果这样就算证明结束,就太委屈它了。为了探索这些情形,我们有必要对于一般的最短路径算法进行彻底的理解。

首先给出一个引理:

lemma 1 (局部化) 对于有向权图上一条最短路径

,它的所有子路径
为相应端点的最短路径。反过来,如果一条路径的所有真子路径都是最短路径,则该路径也为最短路径。

proof:设最短路径为

,反设从
部分不是最短路径。将它分解为三部分:
。设从
的最短路径为
,有

是最短路径矛盾。反之显然。#

这个性质看似简单,实际上它是迪杰斯特拉算法(贪心算法)的核心思想:从局部推断整体。当每一步都做到“最短”时,连接起来的整体也一定是最短。这个性质还有一个特点:在权值非正时仍然成立。

下面规定一个记号:记

为s到u的最短路径长度。

lemma 2 (三角不等式)

proof:这由最短路径定义即可得出。#

上面这个引理保证了Relax函数的合理性,即Relax函数不会把d值降得比真实的最短路径权值还低:

lemma 3 (确界引理)在任何时候,

proof:初始状态时上式显然。每一次用Relax函数更新d值时,都有:

#

下面又是一个本质性的引理,它证明了我们寻找最短路径的过程是“逐渐收敛的”,即如果真的找到一个节点的最短路径后(当然我们不知道是不是真的找到了,除非遍历完整个图),之后的行动不会破坏该节点的最短路径。

lemma 4(收敛性) 对于最短路径

,若在Relax((u,v,w))执行前有
,则执行之后一定有

proof:由Relax定义有

,反过来的不等式由确界引理保证。显然之后任意时刻该式均成立。

下面给出非负图形式的算法证明:(此时不再有路径长度严格递增的条件了)

proof:不妨设图只有一个连通分支(所有点都联通)。如果我们能证明对于

,在u放入S的那一刻起,
成立(由引理知之后也成立)就算是证明完成了。

反设不成立,不妨假设u是S中第一个违背的元素。而u不能是初始节点s(为什么?),所以在u放入S之前,S集合非空。由于u即将被放入S,沿着u.p回溯回去的路径不是最短路径,那么肯定有一条从s到u的最短路径

。由于u尚且不在S中,
作为一条连接了集合
的道路,道路中一定有相邻x, y满足:

可以直观感受到y似乎“就是”u。但是如果这样,我们无疑陷入了正权重图的证明无法自拔。所以我们的目标改为证明

。我们断言,在此刻
。事实上,由于u是第一个将要违背S集规则(S集中元素都找到了最短路径)的节点,必然有
。由收敛引理我们立刻得出

由于权重非负,且

,我们有:

然而在此时我们选择要加入S集合的元素是u而不是y,所以有

所以

,夹逼知
,和反设假设矛盾,即u确实找到了最短路径;沿着u.p回溯回去,经过一些零权重边,一定会和y相遇。

对于负权重情形,首先我们需要排除掉“负圈“,即对于某个节点,在图上绕一圈回来d值变小。这样的节点的最短路径长度为负无穷。如图所示:

0fb484b8076cef567d8ebed134fa22c8.png
节点1的d值会一直下降,进入死循环

对于一般的没有负圈的图,我们也不能轻易应用迪杰斯特拉算法。考虑下图,

d5c9392ab18d7e143064f1818ac2187b.png
从A点出发,计算机被2权重的边欺骗了

可以看出,之所以不能应用迪杰斯特拉算法,还是因为该算法的“局部性”:它会被眼前的状况“欺骗”。贪心算法之所以“贪心”,是因为在某种意义下,后边的情况不可能比前边的“好”,所以只需要做到当下最好就可以了。

那么在什么情况下可以使用迪杰斯特拉算法呢?有一个充分条件:图上所有权重为负值的边都从初始节点发出。为什么这样呢?作为一个思考题目留给读者。

提示:回顾之前非负图的证明,看哪一步需要权重非负的条件!

参考文献:

[1] Thomas H. Cormen: "introduction to algorithms, third edition", the mit press.

[2] 严蔚敏,吴伟民:数据结构(C语言版),清华大学出版社

[3] stackoverflow上的各种回答,以及各种学校的pdf讲义(图片来源,侵删)

[4] 题图:轻音少女封面图片

悄咪咪:平泽唯天下第一!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值