简介
我在之前的文章里有对Dijkstra's Algorithm进行了思路的分析和讨论,但是也提到了一点,就是这个算法它有一点限制,要求图里面所有的边的权值都是非负的。因为如果有权值为负数的边,就打破了原来算法里每次取得的当前最小值是全局最优值这个假定了。那么在图里面真有权值为负数的边时,甚至在某些情况下存在有加权值为负数的环时,我们该怎么来计算单点源的最短路径呢?
这里,我们将对Bellman-Ford算法的过程进行讨论。
算法分析
在具体讨论Bellman-Ford算法之前,我们先针对Dijkstra's算法的局限思考一下。这个算法的思路是每次取到给定源最短的路径去作为扩展更新的方向,要满足它取的当前最短路径就是全局最短路径这个条件,就要求它里面所有边的权值都是正数,否则这个条件就不满足了。现在,如果在图里面有一些边为负数,那么该怎么来处理这个问题呢?
我们先来看一个包含有负数权值边的图:
假定上图中我们的起点节点是0,从它开始我们尝试去计算到其他节点的最短距离。它到节点1的最短距离是14,而从0 -> 1 -> 4的距离是14 - 21 = -7。而另外一块0 -> 2 的距离是10,0 -> 2 -> 3 -> 4的距离是10。它比原来的0 -> 1 - > 4的距离大,所以取-7作为最小距离。这里有一个比较有意思的地方,就是 2 -> 3 -> 4 -> 5 -> 2形成了一个环,而且如果我们从2开始走一圈这个环则发现它的加权值是-4。这意味着如果我从2开始走一圈,到节点2的权值比原来要小4。那么这里就存在一个问题,如果我这边围着这个圈绕无数圈呢?理论上它的最短路径就成了无穷小了。显然,这种情况是不能接受的。
另外一个,既然存在有负数的边打破了原来算法的假定,这里就会出现一个情况。我前面假定的一个最短的路径上的点,在后面通过其他路径遍历过来的时候发现其实更加短。那么这个点原来遍历过的一些后续节点也需要再一次更新。比如下图:
我们从起点1开始,基于最开始的选择,它到2节点的最短距离是1,然后它再遍历到节点4的距离是1 + 4 = 5。但是实际上如果我们走路径1 -> 3 -> 5 -> 2,它的距离则为-2,因此这里有一个更加短的路径。这个时候,在前面的遍历过程中,节点4已经访问过了。但是这里我们碰到了更近的路径,所以需要对节点4的路径值再更新一下。
经过上面这两种情况的讨论,我们估计就有了这么一个基本的思路。就是要求包含有负权值边的图中间单点源到其他节点的最短路径,它不是所有情况下都存在的。如果图中有权值和为负数的环,那么求它的最短路径就没有意义了,所以这种情况要避免。另外,在遍历的过程中,如果碰到有比原来节点更加短的路径,则需要继续去更新后续的节点。尤其是碰到的节点是之前访问过的节点的情况下。
实现考量
经过上述的讨论,我们已经有了一个基本的思路。在这个算法中,首先需要通过某种方式来遍历图。在之前的一些讨论中我们也知道,我们可以选择深度优先或者广度优先遍历。那么选择哪种更加合适呢?要看这个问题里的具体情况。因为这里还要求每次碰到一个路径更短的节点,就要继续去更新这个节点后续的节点。也就是说和这个节点相邻的所有节点。从实现的角度来说,我们要记录这个后续需要访问的节点。如果采用深度优先遍历的话,这些需要以后遍历的节点不太好标记,而此时用广度优先遍历的方式显得更加自然一些。
这里还有一个关键的问题,就是常用的那两种遍历方式都是通过有一个关联的bool类型数组来记录某个节点是否已经被访问过。这样在后续再次碰到这个节点的时候就跳过去。而这里是如果后续碰到了这个节点,但是这个节点的新距离更短就需要重新捋一遍。所以在实现里就不能仅仅是做一个标记就完了。我们在每次碰到一个比之前距离更近的节点时,需要根据它是否在当前访问的队列里来处理。如果它在当前访问的队列里,那么代表在后续肯定会再遍历它,这里就不用把它加入到队列里,否则就需要加入进去。而每次从队列里取一个元素出来的时候,就需要将它的标记设置为false,表示它已经不在这个访问队列里了。从这个角度来说,我们用的这个关联的bool数组不是用来记录它是否被访问过,而是用来记录它是否在访问队列中。
根据上述讨论,我们可以得到一个基础的实现代码如下:
public class BellmanFordSP { private double[] distTo; private DirectedEdge[] edgeTo; private boolean[] onQ; private Queue<Integer> queue; private int cost; private Iterable<DirectedEdge> cycle; public BellmanFordSP(EdgeWeightedDigraph g, int s) { distTo = new double[g.vertices()]; edgeTo = new DirectedEdge[g.vertices()]; onQ = new boolean[g.vertices()]; queue = new LinkedList<>(); for(int v = 0; v < g.vertices(); v++) distTo[v] = Double.MAX_VALUE; distTo[s] = 0.0; queue.add(s); while(!queue.isEmpty() && !this.hasNegativeCycle()) { int v = queue.remove(); onQ[v] = false; relax(v); } } }
上述代码是整个实现的基础,这里定义了一个队列,用于广度优先遍历实现,同时数组boolean[] onQ用来区分元素是否在队列里。hasNegativeCycle作为判断退出的条件之一。
上述代码里relax方法的实现如下:
private void relax(EdgeWeightedDigraph g, int v) { for(DirectedEdge e : g.adj(v)) { int w = e.to(); if(distTo[w] > distTo[v] + e.weight()) { distTo[w] = distTo[v] + e.weight(); edgeTo[w] = e; if(!onQ[w]) { q.add(w); onQ[w] = true; } } if(cost++ % g.vertices() == 0) findNegativeCycle(); } }
实际上上述代码的逻辑也比较好理解,它就是一个近似BFS里取出当前节点后的操作。将该节点距离加上所有邻接节点的权值,看是否比原来的更加短。如果短一些,则这个节点如果不在队列中的话就要加入进去。这里还有一个细节值得注意的就是因为会碰到一些重新遍历原有节点的过程,在实际运行的时候如果又碰上有权值和为负数的环的话,它就进入了一个无穷循环了。所以这里用一个额外的判断。也就是用cost来记录遍历的次数,一旦遍历到图的节点个数那么多次的时候,就判断一次。
为什么要遍历那么多次来判断一次呢?一般来说,如果没有重复遍历的话,在BFS的过程中就相当于将所有的节点都遍历了一遍了。 如果没有的话,就可能已经形成了负值环,也好判断。
这里还有一个细节就是,在图里该怎么判断存在有负值环呢?我们知道之前有一些方法可以判断图中是否存在有环,而是否有负值环的话则是在找到一个环的时候将它所哟边权值加起来看是否为负数,如果为负数则表示存在有,否则就是没有。找环的思路在之前的文章里也有讨论过,一种办法就是用深度优先遍历的方式,每次在一个节点递归调用的时候设置它对应位置的标记值,而在退出的时候设置回去。如果在这个过程中还是碰到了之前标记过的元素,说明形成了一个环。这部分的一个参考实现代码如下:
private void dfs(EdgeWeightedDigraph G, int v) { onStack[v] = true; marked[v] = true; for (DirectedEdge e : G.adj(v)) { int w = e.to(); // short circuit if directed cycle found if (cycle != null) return; // found new vertex, so recur else if (!marked[w]) { edgeTo[w] = e; dfs(G, w); } // trace back directed cycle else if (onStack[w]) { cycle = new Stack<DirectedEdge>(); DirectedEdge f = e; while (f.from() != w) { cycle.push(f); f = edgeTo[f.from()]; } cycle.push(f); // if sum of all items in cycle < 0 return if(sum(cycle) < 0) return; } } onStack[v] = false; }
上述的代码省略了一些细节,不过还是遵循这个基本的思路。这样,整个Bellman-Ford算法的过程就完成了。
总结
总的来说,Bellman-Ford算法的过程类似于BFS的遍历过程,它在遍历过程中将需要加入队列中的元素做一个标记,而从队列中取出的元素则将标记设置回去。在每次碰到一个元素的时候则判断距离的变化,如果距离更近则将这个节点及后续的节点都重新遍历一把。这个实现通过将这个元素加入到队列里就实现了。看似简单的一步却实现了一个很重要的特性。
在循环的过程中还有几个比较麻烦的就是要每隔若干次之后要判断图中间是否存在有权值和为负数的环,以避免程序进入一个无限循环。总的来说,它这几个步骤稍微有点复杂,不过还是不难理解。
参考材料
相关推荐
最短路径算法—Bellman-Ford(贝尔曼-福特)算法分析与实现(CC++),希望对你能有所帮助!
本资料包聚焦于两种解决最短路径问题的算法:Bellman-Ford算法和SPFA(Shortest Path Faster Algorithm)。 **Bellman-Ford算法** Bellman-Ford算法由Richard Bellman在1958年提出,用于求解带负权边的图中源点到...
本篇将详细介绍两种解决最短路径问题的算法:Bellman-Ford算法和SPFA(Shortest Path Faster Algorithm)。 #### 2. Bellman-Ford算法详解 ##### 2.1 算法原理 Bellman-Ford算法能够处理含有负权重边的图,但前提是...
**贝尔曼-福特算法(Bellman-Ford Algorithm)详解** 在图论中,单源最短路径问题是一个经典的问题,即寻找从一个指定顶点(源点)到图中所有其他顶点的最短路径。贝尔曼-福特算法是解决这类问题的一种有效方法,...
此外,Bellman-Ford算法是Shortest Path Faster Algorithm (SPFA)的基础,SPFA是对Bellman-Ford算法的优化,虽然它的时间复杂度不如Dijkstra算法理想,但SPFA在某些特定情况下表现更优,尤其是在处理负权值边的问题...
这些提交的有用性非常有限,因为大多数真实的图问题都是稀疏的,并且大多数可以通过比 Dijkstra 早 4 或 5 年的 Bellman-Ford-Moore (BFM) 算法的变体更有效地解决。 更好的是,BFM 是健壮的,因为它可以处理负弧...
**贝尔曼-福特算法(Bellman-Ford Algorithm)** 贝尔曼-福特算法是一种在加权有向图中寻找从源节点到所有其他节点最短路径的算法。它是由理查德·贝尔曼在1958年提出的,适用于处理带有负权边的图,这是Dijkstra...
队列优化的Bellman-Ford最短路算法(SPFA,Shortest Path Faster Algorithm)是一种在图论中用于寻找有向图中单源最短路径的算法,它是在经典Bellman-Ford算法基础上进行优化得到的。Bellman-Ford算法本身能够处理...
多用户路由系统的分布式Bellman-Ford算法 一种。 开发环境的详细信息我使用eclipse和Java 1.6来开发我的代码。(JavaSE-1.6)不需要其他编码环境。 b。 有关如何运行代码的说明可以在计算机上的多个命令窗口上运行...
在C ++中实现Bellman-Ford算法。 编译步骤: a. I use Xcode to write my lab1 code in my MacBook Pro. Also in my zip file, there is a file named "LAB1.xcodeproj". b. Just directly open it and you ...
Destination-Sequenced Distance Vector is a table-driven routing scheme for ad hoc mobile networks based on the Bellman-Ford algorithm, Use of network simulation tool NS2, TCL coding.
Bellman-Ford algorithm Edmonds-Karp Maximal Flow Push–Relabel algorithm Huffman Coding Word segementation(CHN/GB18030) using HMM and viterbi algorithm. A* algorithm K-Means Knuth–Morris–Pratt ...
using Bellman-Ford algorithm and the efficacy of the same over Dijkstra’s algorithm is also demonstrated by a large set of simulation results. Simulation results also show that a gain in outage ...
贝尔曼-福特算法(Bellman-Ford Algorithm)同样可以处理负权重边,且能够检测出图中是否存在负权重循环。它的主要应用场景是在图中可能存在负权重边的情况下找到最短路径。 **算法步骤:** 1. 将起点到所有其他...
* Bellman-Ford Algorithm 十一、拥塞控制 *拥塞的概念 * 拥塞的原因 * 拥塞控制方法 * 通信量整形(令牌桶算法) 十二、局域网技术 * 局域网的拓扑结构 * 局域网的协议体系结构 * LLC 协议 * 集线器与交换机 * ...
OSPF使用的是SPF (Shortest Path First) 算法,也被称为迪杰斯特拉算法 (Dijkstra's algorithm)。SPF算法的工作原理是从源路由器出发,计算到达网络中各个目的地的最短路径。这些路径会被记录在一个路由表中,用于...
- **Bellman-Ford Algorithm**: 对于有权负边的图,Bellman-Ford算法可以找到最短路径,还能检测负权环,时间复杂度为O(V*E)。 - **Shortest Path Faster Algorithm**: 一种改进的Bellman-Ford算法,利用队列...
SPFA(Shortest Path Faster Algorithm)和Bellman-Ford算法是解决这类问题的两种有效方法,它们都能处理包含负权边的图,而Dijkstra算法则不适用于存在负权边的情况。 SPFA算法是由西南交通大学的段凡丁在1994年...
在使用 SPFA(Shortest Path Faster Algorithm,一种基于队列的 Bellman-Ford 变种)时,这个问题更加明显,因为 SPFA 是以点为中心进行松弛操作,没有源点可能导致某些边无法被正确松弛。 总的来说,差分约束系统...
**贝尔曼-福特算法(Bellman-Ford Algorithm)** 贝尔曼-福特算法是一种在图中寻找最短路径的算法,特别适用于存在负权边的情况。它由理查德·贝尔曼在1958年提出,是解决单源最短路径问题的一种方法。在有向图或无...