热门标签 | 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并发库中的阻塞队列(BlockingQueue)及其典型应用场景。通过具体实例,展示了如何利用LinkedBlockingQueue实现线程间高效、安全的数据传递,并结合线程池和原子类优化性能。 ... [详细]
  • 本文介绍如何利用动态规划算法解决经典的0-1背包问题。通过具体实例和代码实现,详细解释了在给定容量的背包中选择若干物品以最大化总价值的过程。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 1:有如下一段程序:packagea.b.c;publicclassTest{privatestaticinti0;publicintgetNext(){return ... [详细]
  • 题目描述:给定n个半开区间[a, b),要求使用两个互不重叠的记录器,求最多可以记录多少个区间。解决方案采用贪心算法,通过排序和遍历实现最优解。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • 探讨如何通过编程技术实现100个并发连接,解决线程创建顺序问题,并提供高效的并发测试方案。 ... [详细]
  • 本题探讨了一种字符串变换方法,旨在判断两个给定的字符串是否可以通过特定的字母替换和位置交换操作相互转换。核心在于找到这些变换中的不变量,从而确定转换的可能性。 ... [详细]
  • Explore a common issue encountered when implementing an OAuth 1.0a API, specifically the inability to encode null objects and how to resolve it. ... [详细]
  • 本文介绍如何使用Objective-C结合dispatch库进行并发编程,以提高素数计数任务的效率。通过对比纯C代码与引入并发机制后的代码,展示dispatch库的强大功能。 ... [详细]
  • Java 中 Writer flush()方法,示例 ... [详细]
  • 本文介绍了如何使用 Spring Boot DevTools 实现应用程序在开发过程中自动重启。这一特性显著提高了开发效率,特别是在集成开发环境(IDE)中工作时,能够提供快速的反馈循环。默认情况下,DevTools 会监控类路径上的文件变化,并根据需要触发应用重启。 ... [详细]
  • Java 中的 BigDecimal pow()方法,示例 ... [详细]
  • Java 类成员初始化顺序与数组创建
    本文探讨了Java中类成员的初始化顺序、静态引入、可变参数以及finalize方法的应用。通过具体的代码示例,详细解释了这些概念及其在实际编程中的使用。 ... [详细]
  • 主要用了2个类来实现的,话不多说,直接看运行结果,然后在奉上源代码1.Index.javaimportjava.awt.Color;im ... [详细]
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社区 版权所有