SPFA(Shortest Path Faster Algorithm)算法,是西南交通大学段凡丁于 1994 年发表的,其在 Bellman-ford 算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。

算法过程

设立一个队列用来保存待优化的顶点,优化时每次取出队首顶点 u,并且用 u 点当前的最短路径估计值 dist[u] 对与 u 点邻接的顶点 v 进行松弛操作,如果 v 点的最短路径估计值 dist[v] 可以更小,且 v 点不在当前的队列中,就将 v 点放入队尾。这样不断从队列中取出顶点来进行松弛操作,直至队列空为止。(所谓的松弛操作,简单来说,对于顶点 i,把 dist[i] 调整更小。更多解释请参考百科:松弛操作)

而其检测负权回路的方法也很简单,如果某个点进入队列的次数大于等于 n,则存在负权回路,其中 n 为图的顶点数。

代码

  1. #include <iostream>
  2. #include <queue>
  3. #include <stack>
  4. using namespace std;
  5. int matrix[100][100]; // 邻接矩阵
  6. bool visited[100]; // 标记数组
  7. int dist[100]; // 源点到顶点 i 的最短距离
  8. int path[100]; // 记录最短路的路径
  9. int enqueue_num[100]; // 记录入队次数
  10. int vertex_num; // 顶点数
  11. int edge_num; // 边数
  12. int source; // 源点
  13. bool SPFA()
  14. {
  15. memset(visited, 0, sizeof(visited));
  16. memset(enqueue_num, 0, sizeof(enqueue_num));
  17. for (int i = 0; i < vertex_num; i++)
  18. {
  19. dist[i] = INT_MAX;
  20. path[i] = source;
  21. }
  22. queue<int> Q;
  23. Q.push(source);
  24. dist[source] = 0;
  25. visited[source] = 1;
  26. enqueue_num[source]++;
  27. while (!Q.empty())
  28. {
  29. int u = Q.front();
  30. Q.pop();
  31. visited[u] = 0;
  32. for (int v = 0; v < vertex_num; v++)
  33. {
  34. if (matrix[u][v] != INT_MAX) // u 与 v 直接邻接
  35. {
  36. if (dist[u] + matrix[u][v] < dist[v])
  37. {
  38. dist[v] = dist[u] + matrix[u][v];
  39. path[v] = u;
  40. if (!visited[v])
  41. {
  42. Q.push(v);
  43. enqueue_num[v]++;
  44. if (enqueue_num[v] >= vertex_num)
  45. return false;
  46. visited[v] = 1;
  47. }
  48. }
  49. }
  50. }
  51. }
  52. return true;
  53. }
  54. void Print()
  55. {
  56. for (int i = 0; i < vertex_num; i++)
  57. {
  58. if (i != source)
  59. {
  60. cout << source << " 到 " << i << " 的最短距离是:" << dist[i] << ",路径是:" << i;
  61. int t = path[i];
  62. while (t != source)
  63. {
  64. cout << "--" << t;
  65. t = path[t];
  66. }
  67. cout << "--" << source << endl;
  68. }
  69. }
  70. }
  71. int main()
  72. {
  73. cout << "请输入图的顶点数,边数,源点:";
  74. cin >> vertex_num >> edge_num >> source;
  75. for (int i = 0; i < vertex_num; i++)
  76. for (int j = 0; j < vertex_num; j++)
  77. matrix[i][j] = (i != j) ? INT_MAX : 0; // 初始化 matrix 数组
  78. cout << "请输入 " << edge_num << " 条边的信息:\n";
  79. int u, v, w;
  80. for (int i = 0; i < edge_num; i++)
  81. {
  82. cin >> u >> v >> w;
  83. matrix[u][v] = w;
  84. }
  85. if (SPFA())
  86. Print();
  87. else
  88. cout << "存在负权回路!\n";
  89. return 0;
  90. }

运行如下:

  1. /* Test 1 */
  2. 请输入图的顶点数,边数,源点:5 7 0
  3. 请输入 7 条边的信息:
  4. 0 1 100
  5. 0 2 30
  6. 0 4 10
  7. 2 1 60
  8. 2 3 60
  9. 3 1 10
  10. 4 3 50
  11. 0 1 的最短距离是:70,路径是:1--3--4--0
  12. 0 2 的最短距离是:30,路径是:2--0
  13. 0 3 的最短距离是:60,路径是:3--4--0
  14. 0 4 的最短距离是:10,路径是:4--0
  15. /* Test 2 */
  16. 请输入图的顶点数,边数,源点:4 6 0
  17. 请输入 6 条边的信息:
  18. 0 1 20
  19. 0 2 5
  20. 3 0 -200
  21. 1 3 4
  22. 3 1 4
  23. 2 3 2
  24. 存在负权回路!

判断负权回路的证明

如果某个点进入队列的次数大于等于 n,则存在负权回路。为什么偏偏是 n?

对于一个不存在负权回路的图,设其顶点数为 n,我们把图稍微“转换”下,如下图 A:

  • 与源点 0 邻接的点{ 1, 2, 3 }作为第一批次;
  • 与第一批次邻接的点{ 4, 5, 6, 7, 8, 9 }作为第二批次;
  • ......
  • 与第 k-1 批次邻接的点{ ...... }作为第 k 批次。

其中 k≤n-1,当 k=n-1 时,即为上图 B。

每操作完一个批次的点,至少有一个点的最短路径被确定。这里读者只需从 Dijkstra 算法方面来考虑即可。Dijkstra 每次循环都找出 dist[] 里的最小值,可以对应到这里的每个批次。

一个不存在负权回路的图,最多有 n-1 个批次,每做完一个批次至少有一个点的最短路径被确定,即一个点的入队次数不超过 n-1。因为若一个顶点要入队列,则必存在一条权值之和更小的路径,而在最多做完 n-1 个批次后,所有顶点的最短路径都被确定。(这里需要注意的是,如果一个批次中,有多条路径对某顶点进行更新,则该顶点只会被入队一次,这从代码就可以看出)

时间复杂度

对于一个不存在负权回路的图,我们假设其顶点数为 n,边数为 m。

引自 SPFA 论文:考虑一个随机图,运用均摊分析的思想,每个点的平均出度为 \(O(\frac m n)\),而每个点的平均入队次数为 2,因此时间复杂度为 \(O(n⋅\frac m n⋅2)=O(2m)=O(m)\)。

关于上述的“平均入队次数为 2”,2 这个数字从何得来,我也找不到证明,从网上各位朋友对此的一致态度:尚待商榷。但是可以确定的是,SPFA 算法在随机图中的平均性能是优于 Bellman_Ford 算法的。

SPFA 的最佳时间复杂度为 \(O(n)\)。比如上图 B,每个点只入队一次。

接着再看下 SPFA 的最差时间复杂度,它发生在一个完全图中,如下图(为突出重点,其余边未画出),

我们约定,0 点为源点,每次更新完 k 点出队后,k+1​ 点都可以再次对 k 点进行更新并入队,其中 ​1≤ k≤ n-2​。那么我们得出:

0 点,入队 1 次;

1 点,入队 n-1 次;

2 点,入队 n-2 次;

3 点,入队 n-3 次;

.

n-2 点,入队 2 次;

n-1 点,入队 1 次;

因为是完全图,所以每个点的出度为 n-1,因此总的时间复杂度为:

\[(n-1)⋅[1+1+2+3+...+(n-2)+(n-1)]=O(n^3)
\]

由于是完全图,也可以表达成 \(O(nm)\)。很容易看出,SPFA 算法的时间复杂度很不稳定。

单源最短路径(3):SPFA 算法的更多相关文章

  1. 单源最短路径(dijkstra算法)php实现

    做一个医学项目,当中在病例评分时会用到单源最短路径的算法.单源最短路径的dijkstra算法的思路例如以下: 如果存在一条从i到j的最短路径(Vi.....Vk,Vj),Vk是Vj前面的一顶点.那么( ...

  2. [数据结构与算法-15]单源最短路径(Dijkstra+SPFA)

    单源最短路径 问题描述 分别求出从起点到其他所有点的最短路径,这次主要介绍两种算法,Dijkstra和SPFA.若无负权优先Dijkstra算法,存在负权选择SPFA算法. Dijkstra算法 非负 ...

  3. 图论-单源最短路-SPFA算法

    有关概念: 最短路问题:若在图中的每一条边都有对应的权值,求从一点到另一点之间权值和最小的路径 SPFA算法的功能是求固定起点到图中其余各点的的最短路(单源最短路径) 约定:图中不存在负权环,用邻接表 ...

  4. 【算法导论】单源最短路径之Dijkstra算法

    Dijkstra算法解决了有向图上带正权值的单源最短路径问题,其运行时间要比Bellman-Ford算法低,但适用范围比Bellman-Ford算法窄. 迪杰斯特拉提出的按路径长度递增次序来产生源点到 ...

  5. 【算法导论】单源最短路径之Bellman-Ford算法

    单源最短路径指的是从一个顶点到其它顶点的具有最小权值的路径.我们之前提到的广度优先搜索算法就是一种无权图上执行的最短路径算法,即在所有的边都具有单位权值的图的一种算法.单源最短路径算法可以解决图中任意 ...

  6. 单源最短路径:Dijkstra算法(堆优化)

    前言:趁着对Dijkstra还有点印象,赶快写一篇笔记. 注意:本文章面向已有Dijkstra算法基础的童鞋. 简介 单源最短路径,在我的理解里就是求从一个源点(起点)到其它点的最短路径的长度. 当然 ...

  7. 0016:单源最短路径(dijkstra算法)

    题目链接:https://www.luogu.com.cn/problem/P4779 题目描述:给定一个 n 个点,m 条有向边的带非负权图,计算从 s 出发,到每个点的距离. 这道题就是一个单源最 ...

  8. 单源最短路——SPFA算法(Bellman-Ford算法队列优化)

    spfa的算法思想(动态逼近法):     设立一个先进先出的队列q用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路 ...

  9. 单源最短路径问题-Dijkstra算法

    同样是层序遍历,在每次迭代中挑出最小的设置为已知 ===================================== 2017年9月18日10:00:03 dijkstra并不是完全的层序遍历 ...

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

    1.算法标签 BFS 2.算法概念 Bellman-Ford算法有这么一个先验知识在里面,那就是最短路径至多在N步之内,其中N为节点数,否则说明图中有负权值的回路,这样的图是找不到最短路径的.因此Be ...

随机推荐

  1. Martinjingyu的开发环境

    Mac Pro Book一台去年新款,最近这4年多折腾的东西总结下. Mac的包管理器首选HomeBrew,安装如下: ruby -e "$(curl -fsSL https://raw.g ...

  2. 2017蓝桥杯九宫幻方(C++B组)

    题目:九宫幻方    小明最近在教邻居家的小朋友小学奥数,而最近正好讲述到了三阶幻方这个部分,三阶幻方指的是将1~9不重复的填入一个3*3的矩阵当中,使得每一行.每一列和每一条对角线的和都是相同的. ...

  3. Flask 入门(四)

    url反转 当我学习到url反转的时候,看了一个人写的例子,如下: from flask import Flask,url_for app = Flask(__name__)   @app.route ...

  4. linux如何杀死指定进程

    ps aux | grep '进程名称' sudo kill pid

  5. 如何在云开发静态托管中使用Jekyll

    如何在云开发静态托管中使用Jekyll 介绍 Jekyll 是一个简单的博客形态的静态站点生产机器,通过它,我们可以搭建一个完整的可发布的静态博客网站. Jekyll 也可以运行在 GitHub Pa ...

  6. 一、VMware Workstation 15中文破解版 下载与安装(附密钥)

    下载地址: 下载地址VMware Workstation Pro 15.5.0 Build 14665864https://download3.vmware.com/software/wkst/fil ...

  7. kubernates常用命令

    Kubernetes常用操作命令 kubectl log  //查看日志 $ kubectl log myapp-pod –c test kubectl get pods查看pod列表 [root@n ...

  8. AJ学IOS(44)之网易彩票自定义图片在右边的Button_弹出view_ios6,7简单适配

    AJ分享,必须精品 效果: 注意图里面了吗,其实那个效果做起来真的很简单,在iOS中苹果给我们封装的很好,关键是那个按钮 系统的按钮的图片是在左边的,这里我们需要把他调整到右边,然后呢需要我们自己做一 ...

  9. [算法]素数筛法(埃氏筛法&线性筛法)

    目录 一.素数筛的定义 二.埃氏筛法(Eratosthenes筛法) 三.线性筛法 四.一个性质 一.素数筛的定义 给定一个整数n,求出[1,n]之间的所有质数(素数),这样的问题为素数筛(素数的筛选 ...

  10. JUC强大的辅助类讲解--->>>CountDownLatchDemo (减少计数)

    原理: CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞.其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞), ...