热门标签 | 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



推荐阅读
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 本文介绍了在Vue项目中如何结合Element UI解决连续上传多张图片及图片编辑的问题。作者强调了在编码前要明确需求和所需要的结果,并详细描述了自己的代码实现过程。 ... [详细]
  • 006_Redis的List数据类型
    1.List类型是一个链表结构的集合,主要功能有push,pop,获取元素等。List类型是一个双端链表的结构,我们可以通过相关操作进行集合的头部或者尾部添加删除元素,List的设 ... [详细]
  • 模板引擎StringTemplate的使用方法和特点
    本文介绍了模板引擎StringTemplate的使用方法和特点,包括强制Model和View的分离、Lazy-Evaluation、Recursive enable等。同时,还介绍了StringTemplate语法中的属性和普通字符的使用方法,并提供了向模板填充属性的示例代码。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • javascript  – 概述在Firefox上无法正常工作
    我试图提出一些自定义大纲,以达到一些Web可访问性建议.但我不能用Firefox制作.这就是它在Chrome上的外观:而那个图标实际上是一个锚点.在Firefox上,它只概述了整个 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 本文介绍了如何使用python从列表中删除所有的零,并将结果以列表形式输出,同时提供了示例格式。 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
  • 面向对象之3:封装的总结及实现方法
    本文总结了面向对象中封装的概念和好处,以及在Java中如何实现封装。封装是将过程和数据用一个外壳隐藏起来,只能通过提供的接口进行访问。适当的封装可以提高程序的理解性和维护性,增强程序的安全性。在Java中,封装可以通过将属性私有化并使用权限修饰符来实现,同时可以通过方法来访问属性并加入限制条件。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
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社区 版权所有