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



推荐阅读
  • Web开发实践:创建连连看小游戏
    本文详细介绍了如何在Web环境中开发一款连连看小游戏,适合初学者和技术爱好者参考。通过本文,您将了解游戏的基本结构、连线算法以及实现方法。 ... [详细]
  • 题面:P3178[HAOI2015]树上操作好像其他人都嫌这道题太容易了懒得讲,好吧那我讲。题解:第一个操作和第二个操作本质上是一样的&# ... [详细]
  • LeetCode 102 - 二叉树层次遍历详解
    本文详细解析了LeetCode第102题——二叉树的层次遍历问题,提供了C++语言的实现代码,并对算法的核心思想和具体步骤进行了深入讲解。 ... [详细]
  • 本文档旨在提供C语言的基础知识概述,涵盖常量、变量、数据类型、控制结构及函数定义等内容。特别强调了常量的不同类型及其在程序中的应用,以及如何正确声明和使用函数。 ... [详细]
  • 本文探讨了Android系统中联系人数据库的设计,特别是AbstractContactsProvider类的作用与实现。文章提供了对源代码的详细分析,并解释了该类如何支持跨数据库操作及事务处理。源代码可从官方Android网站下载。 ... [详细]
  • 本文总结了在多人协作开发环境中使用 Git 时常见的问题及其解决方案,包括错误合并分支的处理、使用 SourceTree 查找问题提交、Git 自动生成的提交信息解释、删除远程仓库文件夹而不删除本地文件的方法、合并冲突时的注意事项以及如何将多个提交合并为一个。 ... [详细]
  • 来自FallDream的博客,未经允许,请勿转载,谢谢。一天一套noi简直了.昨天勉强做完了noi2011今天教练又丢出来一套noi ... [详细]
  • 本文详细介绍了PHP中的几种超全局变量,包括$GLOBAL、$_SERVER、$_POST、$_GET等,并探讨了AJAX的工作原理及其优缺点。通过具体示例,帮助读者更好地理解和应用这些技术。 ... [详细]
  • 本文详细介绍了在PHP中如何获取和处理HTTP头部信息,包括通过cURL获取请求头信息、使用header函数发送响应头以及获取客户端HTTP头部的方法。同时,还探讨了PHP中$_SERVER变量的使用,以获取客户端和服务器的相关信息。 ... [详细]
  • java datarow_DataSet  DataTable DataRow 深入浅出
    本篇文章适合有一定的基础的人去查看,最好学习过一定net编程基础在来查看此文章。1.概念DataSet是ADO.NET的中心概念。可以把DataSet当成内存中的数据 ... [详细]
  • 本文详细介绍了Socket在Linux内核中的实现机制,包括基本的Socket结构、协议操作集以及不同协议下的具体实现。通过这些内容,读者可以更好地理解Socket的工作原理。 ... [详细]
  • egg实现登录鉴权(七):权限管理
    权限管理包含三部分:访问页面的权限,操作功能的权限和获取数据权限。页面权限:登录用户所属角色的可访问页面的权限功能权限:登录用户所属角色的可访问页面的操作权限数据权限:登录用户所属 ... [详细]
  • 本文提供了一个关于AC自动机(Aho-Corasick Algorithm)的详细解析与实现方法,特别针对P3796题目进行了深入探讨。文章不仅涵盖了AC自动机的基本概念,还重点讲解了如何通过构建失败指针(fail pointer)来提高字符串匹配效率。 ... [详细]
  • 本文详细探讨了HihoCoder平台上的1398号问题——最大权闭合子图的求解方法。通过具体实例,深入分析了最大权闭合子图的概念及其算法实现。 ... [详细]
  • 贡献转移在计算每个元素的作用的时候,我们可以通过反向枚举作用效果,添加到作用元素的身上,这种方法叫做贡献转移。更正式的说, ... [详细]
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社区 版权所有