Johnson 全源最短路径算法 Java实现

  • 算法导入
  • 算法核心
  • 复杂度分析
    • 时间复杂度
    • 空间复杂度
  • 代码实现
  • 参考资料
  • End

算法导入

在之前的文章中,我们讲述了:

  • 经典入门的Dijkstra算法,属于非负权图的单源最短路径算法。Dijkstra算法,经典入门
  • 实现简单但能力有限的Floyd算法,属于全源最短路径算法。Floyd算法,能力有限
  • 能处理负权图但时间稍逊于 DIjkstraBellmanFord算法,属于单源最短路径算法。BellmanFord算法,稍逊但实用
  • 改进版BellmanFord算法,使用队列提高时间的SPFA算法

讲了这么多,可能 帅哥靓女们想问了,有没有一种终极算法,又能处理大图(指结点和边较多),时间上又能满足要求呢!

高低要给各位介绍一下接下来的big bossJohnson算法了。上图,明差异!

算法核心

Johnson 和 Floyd 一样,是一种能求出无负环图上任意两点间最短路径的算法。该算法在 1977 年由 Donald B. Johnson 提出。

由于任意两点间的最短路可以通过枚举起点,跑 n 次 BellmanFord算法解决,时间复杂度为 O(n^2 * m),也可以直接使用Floyd算法,时间复杂度为 O(n^3)

从时间复杂度上考虑,我们肯定希望时间复杂度越小越好。O(n^2 * m)的时间复杂度还能再降吗?既然你可以跑 n 次 BellmanFord,那么能不能跑 n 次 Dijkstra 呢,毕竟后者的时间复杂度更低。这样想没错,但 Dijkstra 算法不能正确求解带负权边的最短路,因此Johnson算法的精髓就是预处理,确保所有边的边权均非负

容易想到的是给所有边的边权同时加上一个正数x,从而让所有边的边权均非负。如果新图上起点到重点的最短路经过了k条边,则讲最短路减去kx即可得到实际的最短路。

但这样的方法是错误的。考虑下图:

1-> 2的最短路为 1 -> 5 -> 3 -> 2,长度为 - 2。但如果把每条边的边权加上5之后。


新图上 1 -> 2 的最短路 为 1 -> 4 -> 2。好像与原来的最短路不一样啊,那可不行啊。

这样不行,那咱们换一种方法。咱高中物理讲到过一个概念——势能
例如重力势能、电势能。势能有两个重要的特点:

  • 势能的变化量只和起点和终点的相对位置有关,而与起点到终点所走的路径无关。
  • 势能的绝对值往往取决于设置的零势能点,但无论将零势能点设置在哪里,两点间势能的差值是一定的。

明白这个含义之后,我们好像发现了新大陆。只要两个点相对位置不变,那么两点间势能差值一定。那么可以这样:

  1. 新建一个虚拟结点 0,可以理解为 零势能点从0点向其他所有点连一条边权为 0的边。通过BellmanFord算法,求出 虚拟结点 0 到 其他结点的最短路,记为 h(i)。这里可以用Dijkstra算法吗?不可以!因为 BellmanFord算法能处理负权图,而Dijkstra算法不能。
  2. 边权值重设。假如原图中 w(u,v) = x,那么重设后的权值为 x + h(u) - h(v)
  3. 接下来 以每个点为起点,跑 n 轮 Dijkstra算法就可以求出任意两点间的最短路了。

那这样为什么正确呢?

咱来证明一下:

假设从 s -> t 之间的 任一条路径为 s -> p1 -> p2 -> ... -> pk -> t,那么这条路的权值总和为:

w(s, p1) + w(p1, p2) + ... + w(pk, t)

将每条边重设权值为 w(u, v) + h(u) - h(v) 后,这条路的权值总和为:

w(s, p1) + h(s) - h(p1) + w(p1, p2) + h(p1) - h(p2) + ... + w(pk, t) + h(pk) - h(t) = w(s,p1) + w(p1, p2) + ... + w(pk, t) + h(s) - h(t)

前后比较可得:无论从 s->t 的哪一条路径, h(s) - h(t)的值是不变的,与势能概念相吻合,因此 原图上的 s->t 的最短路和新图上的s->t 的最短路相对应。

这保证了边权值修改后,两点间的最短路径仍然是原来的最短路径。那么这保证了新图中所有的边权非负吗?因为在非负权图上,Dijkstra 算法能够保证得出正确的结果。

根据三角形不等式,两边之和大于第三边,那么w(1, 2) + h1 - h2 >= 0当且仅当 虚拟结点 0 与 结点重合时,权值和 为 0,由此可以保证任意条边权值都非负!

复杂度分析

在前面分析中,我们已得知该算法的优秀之处了,而且也能够大致算出具体的时间复杂度。

时间复杂度

  • 1次BellmanFord算法,时间复杂度为O(nm)
  • n次Dijkstra算法,时间复杂度为 O(n * mlogm),采用优先队列实现Dijkstra

总时间复杂度为 O(nm) + O(n * mlogm) = O(nmlogm)

空间复杂度

取决于存储图的方式,由于Johnson算法一般用于处理大图,采用邻接表的方式存储图,空间复杂度为 O(m)

代码实现

import java.util.*;
/*** @author LKQ* @date 2022/4/27 23:17* @description 任意两点之间的最短路可以通过枚举起点,跑 n 次 BellmanFord算法实现,时间复杂度为 O(n^2· m)* 由于 Dijkstra算法时间复杂度为 O(M * logM) 因此若使用此算法代替 BellmanFord, 跑 n 次,那么总时间复杂度为 O(N*M*logM)* 在稀疏图上比Floyd算法的时间复杂度也更加优秀。但由于Dijkstra算法不能处理负权图,因此,需要先跑一次 BellmanFord 对原图上的边预处理。*/
public class Solution {/*** 存储图*/List<int[]>[] graph;/*** 存储任意两点之间的最短路*/int[][] dis;/*** 正无穷*/int INF = Integer.MAX_VALUE / 2;/*** Johnson算法。* @param n n个结点,由于需要使用虚拟结点,我们将 原图中点结点编号为 1 - n,虚拟结点 设定 为 0* @param edges e[0] 到 e[1] 的权值 为 e[2]*/public void Johnson(int n, int[][] edges) {// 1. 根据edges 建图graph = new List[n + 1];for (int i = 0; i <= n; i++) {graph[i] = new ArrayList<>();}for (int[] e: edges) {// 如果edges中,结点编号从1到n,那么直接赋值, 若结点编号 从 0到 n-1, 那么结点编号加一再赋值// graph[e[0] + 1].add(new int[] {e[1] + 1, e[2]})graph[e[0]].add(new int[]{e[1], e[2]});}// 2. 初始化dis数组dis = new int[n + 1][n + 1];for (int i = 1; i <= n; i++) {Arrays.fill(dis[i], INF);dis[i][i] = 0;}// 3. 建立虚拟结点0,0到其他结点的权值为 0graph[0] = new ArrayList<>();for (int i = 1; i <= n; i++) {graph[0].add(new int[] {i, 0});}// 4. 跑一次 BellmanFord算法,求出虚拟结点到其他点的最短路,记为 h(u)int[] h = BellmanFord(n, graph);System.out.println("虚拟结点 0 到其他结点的最短路为 " + Arrays.toString(h));if (h != null) {// 5. 将原图中边的权值重新设置为 w + h(u) - h(v)for (int u = 1; u <= n; u++) {for (int[] e: graph[u]) {int v = e[0], w = e[1];e[1] = w + h[u] - h[v];}}// 6. 以每个结点为起点,跑 n 轮 Dijkstra算法。for (int u = 1; u <= n; u++) {dis[u] = Dijkstra(n, u, graph);// 注意,最后的最短路径还需要加上 h(v) - h(u),才能得出原来的最短路径长度for (int v = 1; v <= n; v++) {if (v != u) {dis[u][v] += h[v] -h[u];}}System.out.println("结点" + u + " -> other points " + print(dis[u]));}}else {// 返回了 null, 说明虚拟结点到其他结点的最短路不存在,说明原图中存在负环。System.out.println("图中存在负环,");}}public int[] BellmanFord(int n, List<int[]>[] graph) {// 初始化虚拟结点0 到其他结点的最短路int[] dis = new int[n + 1];Arrays.fill(dis, INF);dis[0] = 0;// 设置flag 标志,判断一轮循环过程中是否发生松弛boolean flag = false;// 4. bellman-Fordfor (int i = 0; i < n; i++) {flag = false;for (int u = 0; u < n; u++) {// 无穷大与常数加减仍然为无穷大// 因此最短路长度为 inf 的点引出的边不可能发生松弛操作if (dis[u] == INF) {continue;}for (int[] e: graph[u]) {int v = e[0], w = e[1];if (dis[u] + w < dis[v]) {dis[v] = dis[u] + w;flag = true;}}}// 没有可以松弛的边就停止算法if (!flag) {break;}}// 第 n 轮循环仍然可以松弛说明s点可以抵达一个负环,那么返回 null, 否则返回 最短路数组disreturn flag ? null : dis;}public int[] Dijkstra(int n, int s, List<int[]>[] graph) {// n个结点,编号 1-nint[] dis = new int[n + 1];Arrays.fill(dis, INF);// 源点到自身的距离为0dis[s] = 0;// 初始化访问标志,默认为falseboolean[] vis = new boolean[n + 1];// 使用优先队列维护最短路径中最短路最小的点,优化时间PriorityQueue<int[]> pq = new PriorityQueue<>((a, b)-> a[1] - b[1]);pq.add(new int[]{s, 0});while (!pq.isEmpty()) {// 弹出最短路长度最小的点数组int[] t = pq.poll();int u = t[0];// 该点访问过,跳过if (vis[u]) {continue;}vis[u] = true;// 遍历所以 u 能够到达的点for (int[] q: graph[u]) {// 下一个点 v, 即其边权值 wint v = q[0], w = q[1];if (dis[v] > dis[u] + w) {// s->v 的距离 > s->u 的距离 + u->v 的距离,更新最短距离dis[v] = dis[u] + w;// 加入优先队列,s->v 的距离 dis[v]pq.add(new int[]{v, dis[v]});}}}return dis;}public String print(int[] dis) {StringBuffer sb = new StringBuffer();sb.append("[");for (int a: dis) {if (a > INF / 2) {sb.append("INF, ");}else {sb.append(a + ", ");}}sb.replace(sb.length() - 2, sb.length(), "");sb.append("]");return sb.toString();}public static void main(String[] args) {int[][] edges = {{1, 4, 2}, {4, 2, -3}, {1, 5, 0}, {5, 3, 2}, {3, 2, -4}};Solution solution = new Solution();solution.Johnson(5, edges);}
}

以下图为例:

建立虚拟结点 0,到其他结点的边权值为 0

以0为源点,跑一次 BellmanFord算法,求得 0 到其他结点的最短路径为 [0,0,-4,0,0,0],下标从 0 开始

那么重新赋权值后,新图的边权值如下:

那么就可以每个点为起点,跑 n 次 Dijkstra算法,求出任意点之间的最短路径了 。注意,最后返回最短路径时,由于原先权值加了h(u)- h(v),那么返回的时候需要加上 h(v) - h(u),这一点尤其需要注意

测试用例全源最短路为:

参考资料

OI Wiki
图灵程序设计丛书 算法 第4版

End

你说懂了爱不是浪漫 我的本分给你安全感
许嵩 《医生》

Johnson 全源最短路径算法 Java实现相关推荐

  1. Johnson 全源最短路径算法

    前言: 因为太久没学图论了,以前学的图论的知识都差不多忘光了.于是我开启了这及其令人痛苦的图论学习生涯. 传送门:大佬的博客~~~~ 上面的博客写的很好,Johnson的核心就是将负边变正.从而可以每 ...

  2. Dijkstra 单源最短路径算法 Java实现

    Dijkstra 单源最短路径算法 Java实现 算法导入 算法核心 复杂度分析 时间复杂度 空间复杂度 代码实现 参考资料 结尾 算法导入 在图论中,求最短路径有一个经典的算法 Dijkstra算法 ...

  3. 单源路径分支界限java_java单源最短路径算法

    . .. .. . 单源最短路径的 Dijkstra 算法: 问题描述: 给定一... 并 应用贪心法求解单源最短路径问题.环境要求对于环境没有特别要求.对于算法实现,可以自由选择 C, C++, J ...

  4. 动态规划在求解全源最短路径中的应用(JAVA)--Floyd算法

    参考图论算法(二)-最短路径的Dijkstra [ 单源 ] 和Floyd[ 多源 ] 解法(JAVA ) 这种算法也叫Floyd-Warshell算法,虽然和Warshell算法名字相近,算法思想也 ...

  5. 图论-全源最短路径-对比Floyd算法与暴力Dijkstra算法

    题目 输入顶点数N,有向边数M,接下来M行输入格式为u,v,w分别代表两个顶点u,v和两点之间边的权值w.输出全源最短路径 输入样例: 6 8 0 1 1 0 3 4 0 4 4 1 3 2 2 5 ...

  6. 从0开始详解 Johnson 全源最短路(P5905 【模板】Johnson 全源最短路)

    问题引入:([模板]Johnson 全源最短路 - 洛谷) ps:(如果不想 情景带入 请直接转跳到Johnson算法详解) 目录 问题引入:([模板]Johnson 全源最短路 - 洛谷) 题目描述 ...

  7. 应用单源最短路径算法解决套利交易问题

    目录 导言 基本定义与延伸 基本概念与定义 概念延伸 最短路径算法 Bellman-Ford算法 问题简述 问题分析 问题求解 第一题 第二题 总结 导言 最短路径问题是图论领域中的核心问题之一,也是 ...

  8. Dijkstra-单源最短路径算法

    Dijkstra-单源最短路径算法 1.算法概述 2.算法实例 3.实战案例 3.1 题目描述 3.2 解题思路与代码实现 1.算法概述   Dijkstra算法用来计算一个点到其他所有点的最短路径的 ...

  9. Bellman-Ford 单源最短路径算法

    Bellman-Ford 单源最短路径算法 Bellman-Ford 算法是一种用于计算带权有向图中单源最短路径(SSSP:Single-Source Shortest Path)的算法.该算法由 R ...

最新文章

  1. IOS tableView删除数据
  2. Java 编程的动态性,第 5 部分: 动态转换类--转载
  3. 获取iPhone型号
  4. nginx 如何显示真实ip
  5. [Leedcode][JAVA][第22题括号生成][DFS][BFS][动态规划]
  6. ssm框架中mysql的分页_SSM框架中mapper层,增删改查,如何实现
  7. Markdown编辑器
  8. 【万字总结】基于多智能体强化学习的《星际争霸II》中大师级水平的技术研究
  9. 欧拉角科普介绍 Roll Pitch Yaw
  10. 计算机软件cae,各种CAE软件介绍
  11. PassMark Software - PC Benchmark and Test Software - 软件和硬件基准测试
  12. SPSS 探索性因素分析与验证性因素分析的区别【SPSS 034期】
  13. 网页游戏是如何快速推广,寻找真实玩家。提高充值消费的。这里都有教程
  14. h5 app开发 教程
  15. matlab逆变换法产生随机数_[原创]Matlab 生成随机数
  16. 手机短信验证码收不到怎么办?原因和解决方案全都在这里
  17. ddl和dml(DDL和DML包含哪些动词)
  18. 我想知道如何用风扇自制水空调?
  19. 新手学编程,如何入门?
  20. 计算机如何分页打印,[计算机]Excel表格如何分页打印、自动分页、取消分页等技巧.doc...

热门文章

  1. 洛谷P4037 [JSOI2008]魔兽地图 题解
  2. Hackthebox - Previse 靶场实战
  3. multiprocessing.freeze_support()介绍
  4. iMazing怎么导出应用程序的存档
  5. OmniPlayer Pro for Mac(全能多媒体播放器)
  6. 题解洛谷P4752 【Divided Prime】
  7. c#使用MethodInvoker解决跨线程访问控件
  8. video画中画(小窗口)
  9. 职业玩家意见将影响星际争霸2平衡设计
  10. 【java】之3种方式实现Object和Map之间的转换