二十六、搜索与图论——SPFA算法(单源最短路 + 负权边 + Bellman-Ford 时间复杂度优化)

news2025/3/12 12:03:48

SPFA算法主要内容

  • 一、基本思路
    • 1、算法概念
    • 2、SPFA 算法步骤
      • 算法步骤
      • 注意事项
    • 3、SPFA算法进行负环判断
  • 二、Java、C语言模板实现
      • SPFA 算法
      • SPFA求负环
  • 三、例题题解

一、基本思路

1、算法概念

  • 概念:

    • SPFA 算法是 Bellman-Ford算法 的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环。SPFA一般情况复杂度是O(m), 最坏情况下复杂度和朴素 Bellman-Ford 相同,为O(nm)。
  • 原 Bellman-Ford 算法步骤 :

    • for n次
      for 所有边 a,b,w (松弛操作)
      dist[b] = min(dist[b],back[a] + w)
    • spfa算法对第二行中所有边进行松弛操作进行了优化,原因是在bellman—ford算法中,即使该点的最短距离尚未更新过,但还是需要用尚未更新过的值去更新其他点,由此可知,该操作是不必要的,我们只需要找到更新过的值去更新其他点即可,此处即为优化点。

2、SPFA 算法步骤

算法步骤

  • 1. queue <– 1
  • 2. while queue 不为空
    • (1) t <– 队头 —— 取队头
      • queue.pop() —— 删掉头结点
    • (2) 用 t 更新所有出边 t –> b,权值为w
      • queue <– b (若该点被更新过,则拿该点更新其他点)

注意事项

    1. 与堆优化版的Dijkstra算法相比:堆优化版使用的是小根堆的排序之后的头结点进行更新(遍历的是距离最近点),SPFA算法是只要该点距离更新了,那就可以加入队列更新其他点。
    1. Dijkstra 算法一般用SPFA算法也可以,但是若为网格图,则可能会卡为O(mn)

3、SPFA算法进行负环判断

  • 求负环的常用方法,基于SPFA,一般都用方法 2:

  • 方法 1: 统计每个点入队的次数,如果某个点入队n次,则说明存在负环

  • 方法 2: 统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于n,则也说明存在环。

  • 每次做一遍spfa()一定是正确的,但时间复杂度较高,可能会超时。

  • 初始时将所有点插入队列中可以按如下方式理解:

    • 在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于视频中的做法了。那么视频中的做法可以找到负环,等价于这次spfa可以找到负环,等价于新图有负环,等价于原图有负环。得证。
  • 求负环算法步骤:

    1. dist[x] 记录虚拟源点到x的最短距离
    1. cnt[x] 记录当前x点到虚拟源点最短路的边数,初始每个点到虚拟源点的距离为0,只要他能再走n步,即cnt[x] >= n,则表示该图中一定存在负环,由于从虚拟源点到x至少经过n条边时,则说明图中至少有n + 1个点,表示一定有点是重复使用
    1. 若dist[j] > dist[t] + w[i],则表示从t点走到j点能够让权值变少,因此进行对该点j进行更新,并且对应cnt[j] = cnt[t] + 1,往前走一步
  • 注意:

  • 该题是判断是否存在负环,并非判断是否存在从1开始的负环,因此需要将所有的点都加入队列中,更新周围的点.

在这里插入图片描述

二、Java、C语言模板实现

SPFA 算法

//Java 模板实现
    static int N = 100010;
    static int idx = 0;
    static int INF = 0x3f3f3f3f;
    static int[] h = new int[N];
    static int[] e = new int[N];
    static int[] ne = new int[N];
    static int[] w = new int[N];
    static int[] dis = new int[N];
    static boolean[] st = new boolean[N];       // 判断是否在更新队列中
    static int n,m;
    static int x,y,c;
    
    static void add(int a, int b, int c){       // 图的存储,其中存在负权边
        e[idx] = b;
        ne[idx] = h[a];
        w[idx] = c;
        h[a] = idx++;
    }
    
    static String spfa(){
        PriorityQueue<Integer> q = new PriorityQueue<Integer>();        // 存储的是所有“更新距离”的点
        // 此处的队列和堆优化的Dijkstra算法的区别在于,堆优化的需要通过小根堆,找到距离最近的点进行更新
        // 而这里使用的是队列存储“更新距离”的点
        
        dis[1] = 0;             // 初始化第一个点的距离为 0 
        q.add(1);               // 将第一个点加入队列
        st[1] = true;           // 标记点 1 在队列中
        
        while(!q.isEmpty()){
            int t = q.poll();
            st[t] = false;      // 表明这个点已经弹出队列中,也就是“不在更新了距离”的队列
            
            for(int i = h[t]; i != -1; i = ne[i]){      // 遍历“更新点”可以连接到的点,用它来更新其他点
                int newNode = e[i];                     // 存的是点b,也就是出边对应点
                
                if(dis[newNode] > dis[t] + w[i]){       // 判断是否可以进行距离更新
                    dis[newNode] = dis[t] + w[i];
                    // 需要使用这个点进行其他点的更新,因此需要加入队列
                    // 判断点是否在队列,如果不在队列中,那就将它加入
                    if(st[newNode] == false){           
                        q.add(newNode);
                        st[newNode] = true;     // 表明这个点在“更新距离”队列
                    }
                }
            }
        }
        
        if(dis[n] == INF){
            return "impossible";
        }
        
        return dis[n] + "";
    }
```cpp
// C++实现,此处是yxc实现
int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}


SPFA求负环

    static int N = 100010;
    static int idx = 0;
    static int INF = 0x3f3f3f3f;
    static int[] h = new int[N];
    static int[] e = new int[N];
    static int[] ne = new int[N];
    static int[] w = new int[N];
    static int[] dis = new int[N];
    static int[] count = new int[N];            // 判断到达这个点,需要经过的路径条数
    static boolean[] st = new boolean[N];       // 判断是否在更新队列中
    static int n,m;
    static int x,y,c;
    
    static void add(int a, int b, int c){       // 图的存储,其中存在负权边
        e[idx] = b;
        ne[idx] = h[a];
        w[idx] = c;
        h[a] = idx++;
    }
    
    static String spfa(){
        PriorityQueue<Integer> q = new PriorityQueue<Integer>();        // 存储的是所有“更新距离”的点
        // 此处的队列和堆优化的Dijkstra算法的区别在于,堆优化的需要通过小根堆,找到距离最近的点进行更新
        // 而这里使用的是队列存储“更新距离”的点
        
        dis[1] = 0;             // 初始化第一个点的距离为 0
        
        // 因为有些存在负环,但是并不能实现从1到负环点
        // 所以就把所有店加入队列中,这样哪怕从1 没有路径到达,但是依旧可以实现遍历进行环的循环
        // 加入到了负环点实际是可以一直转圈圈的死循环,因此可以仅仅使用n来进行判断是否存在负环点
        for(int i = 1; i <= n; i++){
            q.add(i);               // 将点加入队列
            st[i] = true;           // 标记点在队列中
        }
        
        while(!q.isEmpty()){
            int t = q.poll();
            st[t] = false;      // 表明这个点已经弹出队列中,也就是“不在更新了距离”的队列
            
            for(int i = h[t]; i != -1; i = ne[i]){      // 遍历“更新点”可以连接到的点,用它来更新其他点
                int newNode = e[i];                     // 存的是点b,也就是出边对应点
                
                if(dis[newNode] > dis[t] + w[i]){       // 判断是否可以进行距离更新
                    dis[newNode] = dis[t] + w[i];
                    count[newNode] = count[t] + 1;
                    
                    if(count[newNode] >= n){
                        return "Yes";
                    }
                    // 需要使用这个点进行其他点的更新,因此需要加入队列
                    // 判断点是否在队列,如果不在队列中,那就将它加入
                    if(st[newNode] == false){           
                        q.add(newNode);
                        st[newNode] = true;     // 表明这个点在“更新距离”队列
                    }
                    
                }

            }
        }
        
        return "No";
    }
int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N], cnt[N];        // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N];     // 存储每个点是否在队列中

// 如果存在负环,则返回true,否则返回false。
bool spfa()
{
    // 不需要初始化dist数组
    // 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。

    queue<int> q;
    for (int i = 1; i <= n; i ++ )
    {
        q.push(i);
        st[i] = true;
    }

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;       // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

三、例题题解

在这里插入图片描述

// java题解实现
import java.util.*;

public class Main{
    static int N = 100010;
    static int idx = 0;
    static int INF = 0x3f3f3f3f;
    static int[] h = new int[N];
    static int[] e = new int[N];
    static int[] ne = new int[N];
    static int[] w = new int[N];
    static int[] dis = new int[N];
    static boolean[] st = new boolean[N];       // 判断是否在更新队列中
    static int n,m;
    static int x,y,c;
    
    static void add(int a, int b, int c){       // 图的存储,其中存在负权边
        e[idx] = b;
        ne[idx] = h[a];
        w[idx] = c;
        h[a] = idx++;
    }
    
    static String spfa(){
        PriorityQueue<Integer> q = new PriorityQueue<Integer>();        // 存储的是所有“更新距离”的点
        // 此处的队列和堆优化的Dijkstra算法的区别在于,堆优化的需要通过小根堆,找到距离最近的点进行更新
        // 而这里使用的是队列存储“更新距离”的点
        
        dis[1] = 0;             // 初始化第一个点的距离为 0 
        q.add(1);               // 将第一个点加入队列
        st[1] = true;           // 标记点 1 在队列中
        
        while(!q.isEmpty()){
            int t = q.poll();
            st[t] = false;      // 表明这个点已经弹出队列中,也就是“不在更新了距离”的队列
            
            for(int i = h[t]; i != -1; i = ne[i]){      // 遍历“更新点”可以连接到的点,用它来更新其他点
                int newNode = e[i];                     // 存的是点b,也就是出边对应点
                
                if(dis[newNode] > dis[t] + w[i]){       // 判断是否可以进行距离更新
                    dis[newNode] = dis[t] + w[i];
                    // 需要使用这个点进行其他点的更新,因此需要加入队列
                    // 判断点是否在队列,如果不在队列中,那就将它加入
                    if(st[newNode] == false){           
                        q.add(newNode);
                        st[newNode] = true;     // 表明这个点在“更新距离”队列
                    }
                }
            }
        }
        
        if(dis[n] == INF){
            return "impossible";
        }
        
        return dis[n] + "";
    }
    
    public static void main(String[] args){
        Scanner in = new Scanner(System.in);
        n = in.nextInt();
        m = in.nextInt();
        
        for(int i = 0; i < N; i++){
            dis[i] = INF;
            h[i] = -1;
        }
        
        for(int i = 0; i < m; i++){
            x = in.nextInt();
            y = in.nextInt();
            c = in.nextInt();
            add(x, y, c);
        }
        
        String result = spfa();
        
        System.out.println(result);
        
    }
}

在这里插入图片描述

import java.util.*;

public class Main{
    static int N = 100010;
    static int idx = 0;
    static int INF = 0x3f3f3f3f;
    static int[] h = new int[N];
    static int[] e = new int[N];
    static int[] ne = new int[N];
    static int[] w = new int[N];
    static int[] dis = new int[N];
    static int[] count = new int[N];            // 判断到达这个点,需要经过的路径条数
    static boolean[] st = new boolean[N];       // 判断是否在更新队列中
    static int n,m;
    static int x,y,c;
    
    static void add(int a, int b, int c){       // 图的存储,其中存在负权边
        e[idx] = b;
        ne[idx] = h[a];
        w[idx] = c;
        h[a] = idx++;
    }
    
    static String spfa(){
        PriorityQueue<Integer> q = new PriorityQueue<Integer>();        // 存储的是所有“更新距离”的点
        // 此处的队列和堆优化的Dijkstra算法的区别在于,堆优化的需要通过小根堆,找到距离最近的点进行更新
        // 而这里使用的是队列存储“更新距离”的点
        
        dis[1] = 0;             // 初始化第一个点的距离为 0
        
        // 因为有些存在负环,但是并不能实现从1到负环点
        // 所以就把所有店加入队列中,这样哪怕从1 没有路径到达,但是依旧可以实现遍历进行环的循环
        // 加入到了负环点实际是可以一直转圈圈的死循环,因此可以仅仅使用n来进行判断是否存在负环点
        for(int i = 1; i <= n; i++){
            q.add(i);               // 将点加入队列
            st[i] = true;           // 标记点在队列中
        }
        
        while(!q.isEmpty()){
            int t = q.poll();
            st[t] = false;      // 表明这个点已经弹出队列中,也就是“不在更新了距离”的队列
            
            for(int i = h[t]; i != -1; i = ne[i]){      // 遍历“更新点”可以连接到的点,用它来更新其他点
                int newNode = e[i];                     // 存的是点b,也就是出边对应点
                
                if(dis[newNode] > dis[t] + w[i]){       // 判断是否可以进行距离更新
                    dis[newNode] = dis[t] + w[i];
                    count[newNode] = count[t] + 1;
                    
                    if(count[newNode] >= n){
                        return "Yes";
                    }
                    // 需要使用这个点进行其他点的更新,因此需要加入队列
                    // 判断点是否在队列,如果不在队列中,那就将它加入
                    if(st[newNode] == false){           
                        q.add(newNode);
                        st[newNode] = true;     // 表明这个点在“更新距离”队列
                    }
                    
                }

            }
        }
        
        return "No";
    }
    
    public static void main(String[] args){
        Scanner in = new Scanner(System.in);
        n = in.nextInt();
        m = in.nextInt();
        
        for(int i = 0; i < N; i++){
            dis[i] = INF;
            h[i] = -1;
        }
        
        for(int i = 0; i < m; i++){
            x = in.nextInt();
            y = in.nextInt();
            c = in.nextInt();
            add(x, y, c);
        }
        
        String result = spfa();
        
        System.out.println(result);
        
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/539227.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

〖大学生·技术人必学的职业规划白宝书 - 职业规划篇⑤〗- 利用职业能力模型拆解目标岗位

历时18个月&#xff0c;采访 850 得到的需求。 不管你是在校大学生、研究生、还是在职的小伙伴&#xff0c;该专栏有你想要的职业规划、简历、面试的答案。说明&#xff1a;该文属于 大学生技术人职业规划白宝书 专栏&#xff0c;购买任意白宝书体系化专栏可加入TFS-CLUB 私域社…

redis高级篇(2)---主从同步

一)搭建主从架构: 单节点Redis的并发能力是有限的&#xff0c;所以说要想进一步提高Redis的并发能力&#xff0c;就需要搭建主从集群&#xff0c;实现读写分离&#xff0c;因为对于Redis来说大部分都是读多写少的场景&#xff0c;更多的要进行读的压力&#xff0c;最基本都要是…

7.Redis管道/流水线

这里写目录标题 是什么&#xff1f;Redis pipelining 案例演示&#xff1a;小总结Pipeline与原生批量命令对比Pipeline与事务对比使用Pipeline注意事项 &#xff08;Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。一个请求会遵循以下步骤&#xff1a; 1 .客户…

[云原生] 破局微服务通信:探索MegaEase服务网格的创新之路

文章目录 [云原生]破局微服务通信&#xff1a;探索MegaEase服务网格的创新之路解析MegaEase&#xff1a;服务网格领域的领先者 1. MegaEase的背景和使命拓展: 2. MegaEase的核心产品&#xff1a;EaseMesha. 动态服务发现与负载均衡b. 弹性和容错机制c. 安全性与身份认证d. 可观…

M^2BEV: 统一的鸟瞰图表示的多相机联合3D检测和分割

文章&#xff1a;M^2BEV: Multi-Camera Joint 3D Detection and Segmentation with Unified Bird’s-Eye View Representation 作者&#xff1a;Enze Xie, Zhiding Yu, Daquan Zhou, Jonah Philion, Anima Anandkumar, Sanja Fidler, Ping Luo, Jose M. Alvarez 编辑&#xff1…

SpringBoot内置Tomcat 配置和切换

目录 SpringBoot内置Tomcat 配置和切换 基本介绍 内置Tomcat 的配置 application.yml配置 对上面代码解释一下方便理解 通过类来配置Tomcat 注销application.yml 对tomcat 配置&#xff0c;完成测试 切换WebServer, 演示如何切换成Undertow 修改pom.xml , 排除tomcat…

sqli_labs17 ——更新注入

输入用户名admin密码随便输入&#xff0c;发现回显提示是成功修改密码&#xff0c;更新就是指的更新密码 想要在用户栏测试闭合&#xff0c;但是都回显的是hacker 利用自己的字典查了一下也全部回显的一样的数据&#xff0c;那说明这个用户栏是设定了严格的过滤的&#xff0c;结…

前端3D技术概述

操作系统、编译原理、计算机图形学被传为程序员的三大浪漫&#xff0c;每个方向都易懂难深&#xff0c;但作为程序员对每个方向还是要有基本的认识和判断&#xff0c;毕竟贯穿虚拟和现实生活的桥梁是搭建在抽象之上的&#xff0c;这三大浪漫是抽象化的课代表。本次我们认识下计…

Spring 五大类注解、方法注解、对象注入简化 Bean 操作

目录 为什么要使用注解 配置扫描文件&#xff1a; 1.配置扫描路径 1. 五大类注解&#xff1a; 五大类注解有哪些&#xff1f; 1.1 Controller&#xff1a; 1.1.1 存储 Bean 1.1.2 读取 Bean 1.2 Service&#xff1a; 1.2.1 存储 Bean 1.2.2 读取 Bean 1.3 Repository&a…

Hadoop的基本概念和架构

Hadoop的基本概念和架构 学习路线 hadoop的基本概念和架构hadoop的安装和配置hadoop的HDFS文件系统hadoop的MapReduce计算框架hadoop的YARN资源管理器hadoop的高级特效&#xff0c;如HBase&#xff0c;Hive&#xff0c;Pig等hadoop的优化和调优hadoop的应用场景&#xff0c;如…

23种设计模式之组合模式(Composite Pattern)

前言&#xff1a;大家好&#xff0c;我是小威&#xff0c;24届毕业生&#xff0c;在一家满意的公司实习。本篇文章将23种设计模式中的组合模式&#xff0c;此篇文章为一天学习一个设计模式系列文章&#xff0c;后面会分享其他模式知识。 如果文章有什么需要改进的地方还请大佬不…

跨平台应用开发进阶(六十三):微信小程序开发

文章目录 一、前言二、开发物料准备2.1 个性化小程序2.2 开发时如何调用API2.3 微信小程序开发常用组件库2.3.1 WeUI2.3.2 Vant Weapp2.3.3 iView Weapp2.3.4 ColorUI2.3.5 Wux Weapp2.3.6 TaroUI2.3.7 MinUI 三、创建一个UNI-APP项目四、进行调试4.1 启动微信开发者工具4.2 调…

网络基础进阶

1、交换机接口类型 Console口&#xff1a;也称为&#xff1a;串口接口&#xff0c;一般用于与PC连接&#xff0c;用于配置和监控交换机。百兆以太网接口&#xff1a;用于连接计算机和交换机之间的通信。Console到的网络接口&#xff1a;俗称交叉串口&#xff0c;是用于连接交换…

Day.js 常用方法

Day.js是一个极简的JavaScript库&#xff0c;可以为现代浏览器解析、验证、操作和显示日期和时间&#xff0c;文件大小只有2KB左右&#xff0c;下载、解析和执行的JavaScript更少。 官网&#xff1a;Day.js中文网 1. 安装 npm install dayjs --save 2.引入 3. 初始化日期 …

基于开源大模型Vicuna-13B构建私有制库问答系统

本教程专注在怎么使用已经开源的模型和项目&#xff0c;构建一个可以私有化部署的问答知识库&#xff0c;而且整体效果要有所保障。 主要工作包括&#xff1a; 选择基础模型&#xff0c;openAI&#xff0c;claude 这些商用的&#xff0c;或者其他的开源的&#xff0c;这次我们…

vue 全局注册--

注册全局-过滤器 filters 的 js 文件 /*** 格式化单位展示* param value* param unit* returns {string}*/const unitFormatter function (value , unit ) {value value || -return value ([, -].includes(value) ? : unit) }export default {unitFormatter }vue -ma…

如何使用ArcGIS进行字段连接

&#xff08;本文首发于“水经注GIS”公号&#xff0c;关注公号免费领取地图数据 当我们的shapefile文件属性表中的数据不够丰富&#xff0c;而Excel表格中的数据很丰富的时候&#xff0c;我们可以通过两个数据之间的共有字段进行连接&#xff0c;这里为大家介绍一下ArcGIS中字…

五、easyUI中的datagrid(数据表格)组件

1.datagrid&#xff08;数据表格&#xff09;组件的概述 datagrid以表格形式展示数据&#xff0c;并提供了丰富的选择、排序、分组和编辑数据的功能支持。datagrid的设计用于缩短开发时间&#xff0c;并且使开发人员不需要具备特定的知识。它是轻量级的且功能丰富&#xff0c;…

MDM(移动设备管理策略系统)、MAM、MCM作用

目录 定义 MDM MAM MCM 作用 定义 MDM 移动设备管理是一种软件应用程序,用于管理企业中的终端,如笔记本电脑、智能手机、平板电脑等。随着越来越多的员工使用这些设备,各种形式和规模的企业现在都转向移动设备管理,以增强数据安全性并提高生产力。 Device Management…

C嘎嘎~~[构造函数提升篇]

构造函数提升篇 1. 再谈构造函数1.1. 引入1.1.1问题引入1.1.2 const引入 1.2 正篇1.2.1 构造函数体赋值1.2.2 初始化列表1.2.3.1 浅浅认识1.2.3.2 构造函数的 行走顺序1.2.3.3 引用修饰成员变量1.2.3.4 没有默认构造的自定义类型 1.2.3初始化列表的 坑1.2.4 谈谈初始化列表 和 …