热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

数据结构与算法最短路径之Bellman算法、SPFA算法

数据结构与算法--最短路径之Bellman算法、SPFA算法除了Floyd算法,另外一个使用广泛且可以处理负权边的是Bellman-Ford算法。Bellman-Fo

数据结构与算法--最短路径之Bellman算法、SPFA算法

除了Floyd算法,另外一个使用广泛且可以处理负权边的是Bellman-Ford算法。

Bellman-Ford算法

假设某个图有V个顶点E条边。

该算法主要流程是:

  • 初始化。到起点s的距离distTo[s]设置为0,其余顶点的dist[]设置为正无穷;
  • 以任意次序放松图中的所有E条边,重复V轮;
  • V轮放松结束后,判断是否存在负权回路。如果存在,最短路径没有意义。

根据流程可以给出代码,如下

package Chap7;import java.util.LinkedList;
import java.util.List;public class BellmanFord {private boolean hasNegativeCycle;private double distTo[];private DiEdge[] edgeTo;public boolean hasNegativeCycle() {return hasNegativeCycle;}private void relax(EdgeWeightedDiGraph graph, int v) {for (DiEdge edge : graph.adj(v)) {int w &#61; edge.to();if (distTo[v] &#43; edge.weight() graph, int s) {distTo &#61; new double[graph.vertexNum()];edgeTo &#61; new DiEdge[graph.vertexNum()];for (int i &#61; 0; i &#61; distTo[w]// 如果对于图中任意边&#xff0c;仍然存在distTo[v] &#43; edge.weight() pathTo(int v) {if (hasPathTo(v)) {LinkedList path &#61; new LinkedList<>();for (DiEdge edge &#61; edgeTo[v]; edge !&#61; null; edge &#61; edgeTo[edge.from()]) {path.push(edge);}return path;}return null;}public static void main(String[] args) {List vertexInfo &#61; List.of("v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7");int[][] edges &#61; {{4, 5}, {5, 4}, {4, 7}, {5, 7}, {7, 5}, {5, 1}, {0, 4}, {0, 2},{7, 3}, {1, 3}, {2, 7}, {6, 2}, {3, 6}, {6, 0}, {6, 4}};double[] weight &#61; {0.35, 0.35, 0.37, 0.28, 0.28, 0.32, 0.38, 0.26, 0.39, 0.29,0.34, 0.40, 0.52, 0.58, 0.93};EdgeWeightedDiGraph graph &#61; new EdgeWeightedDiGraph<>(vertexInfo, edges, weight);BellmanFord[] all &#61; new BellmanFord[graph.vertexNum()];for (int i &#61; 0; i }

在V轮放松完成后&#xff0c;如果没有负权回路存在&#xff0c;那么对于任何v -> w必然有distTo[v] &#43; edge.weight() >&#61; distTo[w]&#xff0c;说明所有dist[w]已经是最短路径了&#xff1b;如果V轮后还存在distTo[v] &#43; edge.weight() &#xff0c;说明distTo[w]无法收敛到最小值——陷入死循环了&#xff0c;我们围着那个环绕圈子&#xff0c;可以使得路径越来越短&#xff01;这就是遇到了负权回路。

上面的例子没有负权回路存在&#xff0c;我们特意制造一个&#xff0c;看看结果。

public static void main(String[] args) {List vertexInfo &#61; List.of("v0", "v1", "v2", "v3");int[][] edges &#61; {{0, 1}, {1, 2}, {2, 0}, {0, 3}, {2, 3}};double[] weight &#61; {-9 , 5, 2, 4, 6};EdgeWeightedDiGraph graph &#61; new EdgeWeightedDiGraph<>(vertexInfo, edges, weight);BellmanFord bellmanFord &#61; new BellmanFord(graph, 0);if (bellmanFord.hasNegativeCycle()) {System.out.println("路径中存在负权环&#xff01;");}
}

0 -> 1-> 2 -> 0是一个负权回路。这里也注意下&#xff0c;如果图中有边的权值为负&#xff0c;在求最短的路径的时候要先判断有没有负权回路存在再进行后续计算。

SPFA算法--Bellman-Ford算法的优化

其实&#xff0c;根据经验我们很容易知道在任意一轮中许多边都不会放松成功。我们下次需要放松的顶点&#xff0c;只需是上次dist[w]值发生改变的那些w顶点。为此用一个队列保存这些顶点&#xff0c;用一个onPQ[]的布尔数组&#xff0c;来判断某个顶点是否已经在队列中。基于队列优化的Bellm-Ford算法又称为SPFA算法(Shortest Path Faster Algorithm)。

SPFA算法的思路是&#xff1a;每次放松一条边v -> w&#xff0c;如果放松成功&#xff08;即distTo[w]的值被更新&#xff09;&#xff0c;且w没有在队列中则将其入列。然后队列的顶点出列并放松它&#xff0c;直到队列为空或者找到负权回路&#xff0c;算法终止。

这些数据结构可以保证&#xff1a;

  • 队列中不会出现重复的顶点&#xff1b;
  • 在某一轮中&#xff0c;改变了dist[w]和edge[w]的所有w将会在下一轮处理。

如果不存在从起点s可达的负权回路&#xff0c;那么算法终止的条件将是队列为空&#xff1b;如果存在呢&#xff1f;队列永远不会空&#xff08;又在兜圈子了&#xff09;&#xff01;这意味着程序永不会结束&#xff0c;为此&#xff0c;我们必须判断从s可达的路径中是否存在负权回路。如果存在&#xff0c;应该立即停止算法&#xff0c;因为负权回路使得最短路径的研究毫无意义。而且此时经V轮放松后的edgeTo[]中必然会形成一个环&#xff0c;且权值和为负数。但很可能在全部V轮结束前就可以从edgeTo[]中找到负权回路&#xff0c;所以在放松边的过程中&#xff0c;可以隔若干轮就检查一下edgeTo[]中的路径是否成负权回路。

由于不是V轮结束后才检查是否存在负权回路&#xff0c;而是一边放松&#xff0c;一边检查&#xff0c;所以像上面那样用distTo[v] &#43; edge.weight() 的方法来判断已经不适用了&#xff0c;因为放松尚未完成&#xff0c;上式成立很正常&#xff08;说明需要更新最短路径了&#xff09;。于是我们采用一种更通用的方法&#xff1a;先判断是否存在有向环&#xff0c;再判断该环的权值和是不是负数。

寻找有向负权环

判断有向环的实现并不复杂&#xff0c;核心思想其实是DFS&#xff08;深度优先搜索&#xff09;。

package Chap7;import java.util.LinkedList;public class NegativeDiCycle {private boolean[] marked;private DiEdge[] edgeTo;private boolean[] onStack;private LinkedList cycle;public NegativeDiCycle(EdgeWeightedDiGraph graph) {marked &#61; new boolean[graph.vertexNum()];edgeTo &#61; new DiEdge[graph.vertexNum()];onStack &#61; new boolean[graph.vertexNum()];// 有向图可能不是强连通的&#xff0c;所以需要从每个顶点出发&#xff0c;寻找负权环for (int i &#61; 0; i graph, int v) {// 模拟系统递归使用的栈&#xff0c;方法开始进栈&#xff1b;方法结束出栈onStack[v] &#61; true;marked[v] &#61; true;for (DiEdge edge : graph.adj(v)) {// 如果已经存在负权回路&#xff0c;终止递归方法if (this.hasNegativeDiCycle()) {return;}int w &#61; edge.to();if (!marked[w]) {edgeTo[w] &#61; edge;dfs(graph, w);// v -> w的路径&#xff0c;且w在栈中&#xff0c;说明形成有向环} else if (onStack[w]) {cycle &#61; new LinkedList<>();DiEdge e &#61; edgeTo[v];while (e.from() !&#61; w) {cycle.push(e);e &#61; edgeTo[e.from()];}// 为避免空指针&#xff0c;离w最近的那条在循环外入栈cycle.push(e);// 把导致成环的边加入cycle.push(edge);}}onStack[v] &#61; false;}public boolean hasNegativeDiCycle() {if (cycle !&#61; null) {double cycleWeight &#61; cycle.stream().mapToDouble(DiEdge::weight).sum();if (cycleWeight <0) {return true;}}return false;}public Iterable cycle() {if (hasNegativeDiCycle()) {return cycle;}return null;}}

使用DFS的原因主要是为了利用递归产生的由系统维护的栈&#xff08;每次方法调用就相当于入栈&#xff0c;最先调用的最后才返回&#xff09;&#xff0c;而递归方法dfs的调用顺序正好反映了顶点的访问顺序&#xff0c;如先调用dfs(s)&#xff0c; 接着dfs(w), 然后dfs(x)&#xff0c;再递归调用dfs(v)&#xff0c;那么这是一条s -> w -> x -> v的路径。我们使用了一个onStack[]布尔数组来模拟方法调用的进出栈情况——进入方法体说明方法被调用&#xff0c;进栈&#xff1b;方法执行完毕&#xff0c;该返回到上一层方法调用中了&#xff0c;出栈。onStack[]其实就是一条路径&#xff0c;onStack[v] &#61; true说明顶点v位于从起点s可达的onStack[]这条路径中。

该实现最为关键的就是理解&#xff1a;当我们在v处发现某条v -> w的边&#xff0c;而恰好其w位于onStack[]中&#xff0c;就找到了一个环。我们知道onStack[]表示的是s -> w -> x -> v的路径&#xff0c;现在v -> w 刚好补全w -> x -> v成为环&#xff01;如下图所示

IMG_20170925_204315.jpg

好&#xff0c;寻找到有向环后&#xff0c;再判断环内所有边的权值是不是负数就好了。该实现不仅能判断&#xff0c;还能找出到底是哪些边造成了环。关键是以下几行

DiEdge e &#61; edgeTo[v];
while (e.from() !&#61; w) {cycle.push(e);e &#61; edgeTo[e.from()];
}
// 为避免空指针&#xff0c;离w最近的那条在循环外入栈
cycle.push(e);
// 把导致成环的边加入
cycle.push(edge);

对照着上图&#xff0c;最先x -> v入栈&#xff0c;到w -> x时候&#xff0c;发现e.from()就是w&#xff0c;不存入。出了while循环&#xff0c;将这条w -> x入栈&#xff0c;最后别忘了把导致成环的那条边入栈。有人可能会说了为何要这么麻烦&#xff0c;循环外单独push了两次&#xff0c;这是因为edgeTo[]中有的值是null&#xff08;比如起点s&#xff09;&#xff0c;如果不在合适的地方终止循环&#xff0c;将在e &#61; edgeTo[e.from()]该语句执行后&#xff0c;在e.from() !&#61; w处引起空指针异常&#xff01;

SPFA算法的实现

可以判断负权回路的是否存在了&#xff0c;据此实现SPFA算法。

package Chap7;import java.util.*;public class SPFA {private double[] distTo;private DiEdge[] edgeTo;private Queue queue;private boolean[] onPQ; // 顶点是否在queue中private int cost; // 记录放松了边的次数private Iterable cycle; // 找到的负权回路public boolean hasNegativeCycle() {return cycle !&#61; null;}private void findNegativeCycle() {EdgeWeightedDiGraph g &#61; new EdgeWeightedDiGraph<>(edgeTo.length);for (int v &#61; 0; v graph, int v) {for (DiEdge edge : graph.adj(v)) {int w &#61; edge.to();if (distTo[v] &#43; edge.weight() graph, int s) {distTo &#61; new double[graph.vertexNum()];edgeTo &#61; new DiEdge[graph.vertexNum()];queue &#61; new LinkedList<>();onPQ &#61; new boolean[graph.vertexNum()];for (int i &#61; 0; i cycle() {return cycle;}public double distTo(int v) {return distTo[v];}public boolean hasPathTo(int v) {return distTo[v] !&#61; Double.POSITIVE_INFINITY;}public Iterable pathTo(int v) {if (hasPathTo(v)) {LinkedList path &#61; new LinkedList<>();for (DiEdge edge &#61; edgeTo[v]; edge !&#61; null; edge &#61; edgeTo[edge.from()]) {path.push(edge);}return path;}return null;}public static void main(String[] args) {List vertexInfo &#61; List.of("v0", "v1", "v2", "v3");int[][] edges &#61; {{0, 1}, {1, 2}, {2, 0}, {0, 3}, {2, 3}};double[] weight &#61; {-9 , 5, 2, 4, 6};EdgeWeightedDiGraph graph &#61; new EdgeWeightedDiGraph<>(vertexInfo, edges, weight);SPFA spfa &#61; new SPFA(graph, 0);if (spfa.hasNegativeCycle()) {System.out.print("存在负权环&#xff1a;");System.out.println(spfa.cycle());}}
}

程序将输出找到的负权回路&#xff0c;打印[(2->0 2.0), (0->1 -9.0), (1->2 5.0)]。对于不存在负权回路的图&#xff0c;SPFA当然也能正确处理。这里就不测试了。

代码中特别注意一点&#xff0c;我们之前有提到需要隔若干次就检查是否存在负权回路&#xff0c;所以用到一个int型的cost变量记录放松边的次数&#xff0c;每放松了V条边就检查一次。因为可能在第V次放松之后&#xff0c;edgeTo[]数组中就存在负权回路了。findNegativeCycle方法就是将edgeTo[]转化成了有向图送给NegativeDiCycle类&#xff0c;检测是否存在负权回路。


by &#64;sunhaiyu

2017.9.26

转:https://www.cnblogs.com/sun-haiyu/p/7595012.html



推荐阅读
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 第四章高阶函数(参数传递、高阶函数、lambda表达式)(python进阶)的讲解和应用
    本文主要讲解了第四章高阶函数(参数传递、高阶函数、lambda表达式)的相关知识,包括函数参数传递机制和赋值机制、引用传递的概念和应用、默认参数的定义和使用等内容。同时介绍了高阶函数和lambda表达式的概念,并给出了一些实例代码进行演示。对于想要进一步提升python编程能力的读者来说,本文将是一个不错的学习资料。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 也就是|小窗_卷积的特征提取与参数计算
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了卷积的特征提取与参数计算相关的知识,希望对你有一定的参考价值。Dense和Conv2D根本区别在于,Den ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • 本文讨论了一个数列求和问题,该数列按照一定规律生成。通过观察数列的规律,我们可以得出求解该问题的算法。具体算法为计算前n项i*f[i]的和,其中f[i]表示数列中有i个数字。根据参考的思路,我们可以将算法的时间复杂度控制在O(n),即计算到5e5即可满足1e9的要求。 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文介绍了C++中省略号类型和参数个数不确定函数参数的使用方法,并提供了一个范例。通过宏定义的方式,可以方便地处理不定参数的情况。文章中给出了具体的代码实现,并对代码进行了解释和说明。这对于需要处理不定参数的情况的程序员来说,是一个很有用的参考资料。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
author-avatar
平凡兔兔2006
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有