Bellman-Ford算法的DAG视角

概述

Bellman-Ford算法是一种“单源多目标“的通用的计算最短路径的算法。这篇文章并不是详细讲解Bellman-Ford算法的,而是介绍一种“从DAG算法逐步演化出Bellman-Ford算法”的方法。这个知识点是我从参考资料里的视频获得的,有兴趣的同学可以查看相关视频。这里主要记录我的理解和总结。

一句话概括这篇文章的核心内容:“循环图变DAG”+“DAG算法” = “Bellman-Ford算法”

预备知识

只有了解相关预备知识的前提下,才可以更好的了解这篇文章的内容。这里只是最简单的介绍相关预备知识。如果需要,可以先查阅相关预备知识的详细资料,再继续。

DAG算法

DAG(Directed Acyclic Graph)是有向无环图。DAG算法是一种“单源多目标”的针对DAG的计算最短路径的算法。DAG算法可以通过动态规划(Dynamic Programming)原则得出。一个关键点是d[v]=min(d[u] + w(u, v));另一个关键点是拓扑排序(Topological Sort),需要在拓扑排序下执行相关计算。DAG算法的代码如下显示。

DAG-SHORTEST-PATHS(G, w, s)
    topologically sort the vertices of G
    INITALIZE-SINGLE-SOURCE(G, s)
    for each vertex u, taken in topologically sorted order
        do for each edge (u, v) ∈ E[G]
            do RELAX(u, v, w)

一些符号的说明以下(DAG算法、Bellman-Ford算法等都会用到):

  • d[v]。表示从源节点到节点v的暂时的最短路径。当算法完成时,即为最终计算的最短路径。另外,在初始化逻辑里,d[源节点]=0,d[其他节点]=+∞。
  • w(u, v)。有向边(u, v)的权重。
  • RELAX(u, v, w)。if d[v] > d[u] + w(u, v) then d[v] = d[u] + w(u, v)。
  • |V|或|V[G]|。表示图里一共有多少个节点。

DAG算法的运行时间为O(V + E),存储空间为O(V)。

Bellman-Ford算法

Bellman-Ford算法是一种“单源多目标“的通用的计算最短路径的算法。可以适应“负权重(negative weight)”,“正环(positive cycle)”的情况;虽然无法适应“负环(negative cycle)”的情况,但可以检测出来是否存在“负环”。

Bellman-Ford算法如下显示。代码主要分3个部分。第1个部分为初始化逻辑;第2个部分(第1个循环)是用来计算从源点到其他点的最短路径的核心逻辑;第3个部分(第2个循环)是用来检测“负环”的情况。这篇文章要讲述的是第2个部分的逻辑由来。

BELLMAN-FORD(G, w, s)
	// part 1
    INITIALIZE-SINGLE-SOURCE(G, s)
    // part 2
    for i <- 1 to |V[G]| - 1
        do for each edge (u, v) ∈ E[G]
            do RELAX(u, v, w)
    // part 3
    do for each edge (u, v) ∈ E[G]
        do if d[v] > d[u] + w(u, v)
            then return FALSE
    return TRUE

BellmanFord算法的运行时间为O(VE),存储空间为O(V)。

从DAG算法到Bellman-Ford算法

通过以下的例子来说明。这个图,共有4个节点,即|V|=4;v0为源节点 ;这个图有一个“正环”。
graph with positive cycle

1. 循环图变DAG

1)如何将循环图变成DAG呢?这里的关键就是:增加一个时间维度。在每个时刻里,都有|V|个节点。原来的图里的有向边(u, v)就表示:在某个时刻i里的节点u可以去到下一个时刻i+1里的节点v。这时,我们可以得到以下的图。可以看到,在增加一个时间维度后,原来图里的循环没有了。
to_dag_step1
2)一共需要多少个时刻呢?即,|V|个节点要复制多少次呢?答案是一共需要|V|-1次(Bellman-Ford算法里的|V|-1元素出现了)。因为我们要得到的最短路径是最短的简单路径(简单路径就是不包括循环的路径,即,路径包括的每个节点仅能出现一次)。而在一个有|V|个节点的图里,对于任何情况,最长的简单路径最多可以有|V|-1条边(如果存在|V|-1条边的最长的简单路径,这个最长的简单路径就是包括各个节点的从源节点开始的简单路径)。对于我们的例子,|V|=4,因此复制|V|-1=3次。这时,我们可以得到以下的图。
在这里插入图片描述
3)为了在不改DAG算法的初始化逻辑(在初始化逻辑里,d[源节点]=0,d[其他节点]=+∞),可以增加一种特别的边:从某个时刻的节点v到下一个时刻的节点v。而且,这种边的权重为0。这时,我们可以得到以下的图。
在这里插入图片描述
这时,我们已经将一个循环图变身为DAG了!!

2. 使用DAG算法

有了DAG,我们就可以直接使用DAG算法来求“单源多目标“的最短路径了。

在使用DAG算法前,需要留意到新得到的DAG有一个特点,其拓扑排序十分明了,肉眼可见,就是“同层随便,从上到下”。一共有|V|-1层,所以需要:for i <- 1 to |V[G]| - 1;每层有|V|个节点,并且根据原有的边从某层到下一层,所以需要:do for each edge (u, v) ∈ E[G]。大家有没有发现,这时,在不知不觉中,除了一些细节,我们已经将DAG算法演化出Bellman-Ford算法了!!

现在可以对新的DAG开始使用DAG算法了。对于我们的例子,对于DAG算法的初始化逻辑,会有d[v0_0]=0,d[其他节点]=+∞。执行完DAG算法后,对结果做以下的简单的处理就可以得到原来的图的最短路径了:d[vn] = min(d[vn_k]), 其中,n=[1,|V|-1], k=[1,|V|-1]。例如,对于节点v1,最短路径d[v1] = min(d[v1_1], d[v1_2])。在教材里,这里的d[vn]就是“最多使用k条边的从源点到某个其他节点的最短路径”。另外,d[vn_k]可以表示“使用k条边的从源点到某个其他节点的最短路径”。

3. 优化1(循环图变DAG的小优化)

如果稍微修改一下DAG算法的初始化逻辑,可以稍微简化一下新得出来的DAG。

对新的DAG去除掉新增加的特别的边:从某个时刻的节点v到下一个时刻的节点v(即,不需要“1. 循环图变DAG-3)”里的操作)。这时,新的DAG变回“1. 循环图变DAG-2)”里的结果。
在这里插入图片描述
对DAG算法的初始化逻辑做相应的修改:d[v0_k]=0,d[其他节点]=+∞,其中,k=[0,|V|-1]。其他逻辑完全一样。同样得出相应的结果。

4. 优化2(存储空间优化)

按现在的方式,我们需要的存储空间为O(V’)=O(V*(V-1))=O(V^2)。然而,根据我们的拓扑排序是“同层随便,从上到下”,从而下一层的计算只需要依赖于上一层的计算结果,因此,我们只需要O(V)的存储空间就可以了。

这时,在新的DAG上运行DAG算法的运行时间和存储空间如下:

  • 运行时间:O(V’+E’)=O(V*(V-1)+(V-1)*E)=O(V^2+VE)=O(VE)
  • 存储空间:O(V)

DAG算法->Bellman-Ford算法,DONE!!

结语

通过上述方式,可以得出:“循环图变DAG”+“DAG算法” = “Bellman-Ford算法”。这个视角又简单又好理解又好记。Bellman-Ford算法想忘也忘不了了。

参考资料

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值