使用 Dijkstra 算法的负权重

Posted

技术标签:

【中文标题】使用 Dijkstra 算法的负权重【英文标题】:Negative weights using Dijkstra's Algorithm 【发布时间】:2011-10-11 13:36:25 【问题描述】:

我试图理解为什么 Dijkstra 的算法不适用于负权重。阅读Shortest Paths 上的示例,我试图找出以下场景:

    2
A-------B
 \     /
3 \   / -2
   \ /
    C

来自网站:

假设边都是从左到右的,如果我们开始 对于 A,Dijkstra 算法将选择边 (A,x) 最小化 d(A,A)+length(edge),即(A,B)。然后它设置 d(A,B)=2 并选择 另一个边 (y,C) 最小化 d(A,y)+d(y,C);唯一的选择是(A,C) 它设置 d(A,C)=3。但它永远找不到从 A 到的最短路径 B,通过C,总长度为1。

我不明白为什么使用 Dijkstra 的以下实现,d[B] 不会更新为1(当算法到达顶点 C 时,它将在 B 上运行松弛,看到 d[B]等于2,因此将其值更新为1)。

Dijkstra(G, w, s)  
   Initialize-Single-Source(G, s)
   S ← Ø
   Q ← V[G]//priority queue by d[v]
   while Q ≠ Ø do
      u ← Extract-Min(Q)
      S ← S U u
      for each vertex v in Adj[u] do
         Relax(u, v)


Initialize-Single-Source(G, s) 
   for each vertex v  V(G)
      d[v] ← ∞
      π[v] ← NIL
   d[s] ← 0


Relax(u, v) 
   //update only if we found a strictly shortest path
   if d[v] > d[u] + w(u,v) 
      d[v] ← d[u] + w(u,v)
      π[v] ← u
      Update(Q, v)

谢谢,

梅尔

【问题讨论】:

通常使用负边权重的寻路非常困难。无论您找到哪条路线,总有可能出现一条任意长的路线,沿其某处具有任意大的负边权重。如果它是 NP 完整的,我不会感到惊讶。 对于其他有此疑问的人,您可以在图中找到最短路径,因为它没有负权重循环。如果在松弛实际成功时松弛函数返回“真”值,则上述算法将起作用,在这种情况下,如果相邻顶点“v”不存在,则将在优先级队列中排队,或者如果已经存在则更新。这意味着被访问的节点可以再次添加到优先级队列中,因为它们会不断放松。 【参考方案1】:

您建议的算法确实会在此图中找到最短路径,但一般不是所有图。例如,考虑这个图表:

让我们跟踪算法的执行情况。

    首先,将 d(A) 设置为 0,将其他距离设置为 ∞。 然后展开节点 A,将 d(B) 设置为 1,d(C) 设置为 0,d(D) 到 99。 接下来,展开C,没有任何净变化。 然后展开B,没有任何效果。 最后,展开 D,将 d(B) 更改为 -201。

请注意,在此结束时,d(C) 仍然为 0,即使到 C 的最短路径长度为 -200。这意味着您的算法不会计算到所有节点的正确距离。此外,即使您要存储说明如何从每个节点到起始节点 A 的反向指针,您也会最终选择从 C 的错误路径em>A.

原因是 Dijkstra 的算法(和您的算法)是 贪心算法,它们假设一旦计算了到某个节点的距离,找到的距离应该是最优距离。换句话说,该算法不允许自己获取已扩展节点的距离并更改该距离。在负边的情况下,您的算法和 Dijkstra 的算法可能会因为看到负成本边而感到“惊讶”,这确实会降低从起始节点到其他节点的最佳路径的成本。

希望这会有所帮助!

【讨论】:

补充您的出色答案:Dijkstra 是 greedy algorithm 是其短视选择的原因。 我想指出,从技术上讲,由于负循环 A、D、B、A,此图中的所有路径都具有负无穷大的成本。 @Nate- 澄清一下,图中的所有边都是从左到右的。在我的高质量 ASCII 艺术作品中渲染箭头有点困难。 :-) 对于那些以前没有看过带有负边的图的人,我发现这个图的一个有用的解释是收费公路网络,其中边权重给出了你支付的通行费。 -300 路是一条疯狂的向后收费公路,他们会给你 300 美元。 @SchwitJanwityanujit- 这就是 Dijkstra 算法的工作原理。该算法不探索路径,而是通过处理节点来工作。每个节点只处理一次,所以一旦我们处理 B 节点并得到它的距离为 1,我们将永远不会重新访问节点 B 或尝试更新它的距离。【参考方案2】:

请注意,如果图形没有负循环,即总权重小于零的循环,Dijkstra 甚至适用于负权重。

当然有人可能会问,为什么在 templatetypedef Dijkstra 的例子中,即使没有负循环,甚至没有循环,也会失败。那是因为他正在使用另一个停止标准,一旦到达目标节点(或所有节点都已解决一次,他没有具体说明),该标准就保持算法。在没有负权重的图中,这可以正常工作。

如果使用替代停止标准,当优先级队列(堆)运行为空时停止算法(此停止标准也用于问题中),那么即使对于带有负数的图,dijkstra 也会找到正确的距离权重但没有负循环。

但是,在这种情况下,对于没有负循环的图,dijkstra 的渐近时间界限会丢失。这是因为当由于负权重而找到更好的距离时,可以将先前确定的节点重新插入堆中。此属性称为标签校正。

【讨论】:

2.目前尚不清楚为什么您认为时间会“更像贝尔曼福特”而不是指数型(比贝尔曼福特更糟糕)。你有具体的算法和证明吗? To 1.: 因为你可以使用与提到的停止标准完全相同的dijkstra实现,当队列空时停止(参见原始问题中的伪代码),它仍然是dijkstras算法最短路径,即使它的行为不同,多次建立节点(标签更正)。 To 2.: 这只是一个猜测,所以我将删除它。我认为您对指数时间的看法是正确的,因为有许多路径需要探索。【参考方案3】:

TL;DR:答案取决于您的实施。对于您发布的伪代码,它适用于负权重。


Dijkstra 算法的变体

关键是Dijkstra算法有3种实现方式,但是这个问题下的所有答案都忽略了这些变体之间的差异。

    使用嵌套的for-loop 来放松顶点。这是实现 Dijkstra 算法的最简单方法。时间复杂度为 O(V^2)。 基于优先级队列/堆的实现 + 不允许重新进入,其中 重新进入意味着可以将放松的顶点再次推入优先级队列,以便稍后再次放松。 基于优先队列/堆的实现 + 允许重入。

第 1 版和第 2 版在权重为负的图上会失败(如果您在这种情况下得到正确答案,那只是巧合),但第 3 版仍然有效

原问题下贴出的伪代码是上面的版本3,所以是负权重的。

这是来自Algorithm (4th edition) 的一个很好的参考,它说(并包含我上面提到的版本 2 和 3 的 java 实现):

问。 Dijkstra 算法是否适用于负权重?

A.是和不是。有两种最短路径算法称为 Dijkstra 算法,具体取决于顶点是否可以多次在优先级队列中排队。当权重为非负时,两个版本重合(因为没有顶点会被多次排队)。在 DijkstraSP.java 中实现的版本(允许一个顶点多次入队)在存在负边权重(但没有负循环)的情况下是正确的,但在最坏的情况下它的运行时间是指数级的。 (我们注意到,如果边加权有向图的边权重为负,则 DijkstraSP.java 会引发异常,因此程序员不会对这种指数行为感到惊讶。)如果我们修改 DijkstraSP.java 以使顶点不能入队不止一次(例如,使用marked[] 数组来标记那些已经松弛的顶点),那么该算法可以保证在E log V 时间内运行,但是当存在具有负权重的边时,它可能会产生不正确的结果。


更多实现细节以及版本3与Bellman-Ford算法的联系请见this answer from zhihu。这也是我的回答(但用中文)。目前我没有时间把它翻译成英文。如果有人能做到这一点并在***上编辑这个答案,我真的很感激。

【讨论】:

【参考方案4】:

您没有在算法中的任何地方使用 S(除了修改它)。 dijkstra 的想法是一旦一个顶点在 S 上,它就不会再被修改。在这种情况下,一旦 B 在 S 中,您将无法通过 C 再次到达它。

这一事实确保了 O(E+VlogV) 的复杂性 [否则,您将多次重复边,多次重复顶点]

换句话说,您发布的算法可能不像 dijkstra 的算法所承诺的那样在 O(E+VlogV) 中。

【讨论】:

另外,没有负权边的顶点也不需要修改,完全打破了路径成本只能随着重复边增加的假设 这个假设正是允许我们使用 S 的原因,并且“知道”一旦一个顶点在 S 中,它就永远不会再被修改了。 你最后的说法是错误的。当它在没有负边的图上工作时,发布的算法具有时间复杂度 O(E + VlogV)。没有必要检查我们是否已经访问过一个节点,因为它已经被访问过这一事实保证了松弛过程不会在队列中再添加一次。【参考方案5】:

由于 Dijkstra 是一种贪心方法,一旦一个顶点被标记为对该循环已访问,即使以后有另一条成本更低的路径到达它,它也永远不会再次被重新评估。只有当图中存在负边时,才会发生这样的问题。


贪心算法,顾名思义,总是做出当时看起来最好的选择。假设您有一个目标函数,需要在给定点进行优化(最大化或最小化)。 贪心算法在每一步都做出贪心选择,以确保目标函数得到优化。 贪心算法只有一次机会来计算最优解,因此它永远不会返回和逆转决策。

【讨论】:

【参考方案6】:

考虑一下如果你在 B 和 C 之间来回移动会发生什么......瞧

(仅当图没有方向时才相关)

编辑: 我认为问题与以下事实有关边缘,一旦您在经过 AC 后选择到达 B,就不可能找到比 AB 更好的路径。

【讨论】:

这是不可能的,图是有向的。 @amit:好点,我错过了。是时候重新考虑问题了【参考方案7】:

"2) 我们可以使用 Dijksra 算法来计算具有负权重的图的最短路径吗?一个想法可以是,计算最小权重值,将正值(等于最小权重值的绝对值)添加到所有权重并运行修正图的 Dijksra 算法。这个算法能工作吗?"

除非所有最短路径都具有相同的长度,否则这绝对行不通。例如,给定一条长度为两条边的最短路径,在每条边添加绝对值后,总路径成本增加 2 * |最大负权重|。另一方面,另一条长度为三边的路径,因此路径成本增加了 3 * |最大负权重|。因此,所有不同的路径都增加了不同的数量。

【讨论】:

【参考方案8】:

您可以在不包括负循环的负边上使用 dijkstra 算法,但是您必须允许一个顶点可以被多次访问,并且该版本将失去它的快速时间复杂度。

在这种情况下,实际上我发现最好使用SPFA algorithm,它有正常的队列并且可以处理负边缘。

【讨论】:

【参考方案9】:

我只是将所有的 cmets 结合起来,以便更好地理解这个问题。

有两种使用 Dijkstra 算法的方法:

    标记已经找到与源的最小距离的节点(更快的算法,因为我们不会重新访问已经找到最短路径的节点)

    不标记已经找到与源的最小距离的节点(比上面慢一点)

现在问题来了,如果我们不标记节点以便我们找到包括那些负权重的最短路径呢?

答案很简单。考虑一个在图中只有负权重的情况:

)

现在,如果您从节点 0(Source)开始,您将有如下步骤(这里我没有标记节点):

    0->0 为 0, 0->1 为 inf , 0->2 为 inf 开头

    0->1 为 -1

    0->2 为 -5

    0->0 为 -8(因为我们不放松节点)

    0->1 为 -9 .. 以此类推

这个循环将永远持续下去,因此 Dijkstra 的算法在负权重的情况下无法找到最小距离(考虑所有情况)。

这就是为什么使用Bellman Ford Algo来寻找负权重的最短路径,因为它会在负循环的情况下停止循环。

【讨论】:

以上是关于使用 Dijkstra 算法的负权重的主要内容,如果未能解决你的问题,请参考以下文章

Dijkstra算法之 Java详解

Dijkstra算法(Swift版)

Dijkstra 的最短路径算法不返回权重最小的最短路径

为啥 Dijkstra 的算法不适用于负权重边缘?

Dijkstra 算法随机选择具有相同最小权重的相邻节点

求dijkstra算法的C实现