SPFA 算法:实现原理及其应用

news2024/11/27 22:21:42

文章目录

    • 一、前言
    • 二、SPFA 算法
      • 1、SPFA算法的基本流程
      • 2、代码详解
    • 三、SPFA 算法已死 ?

一、前言

SPFA算法,全称为Shortest Path Faster Algorithm,是求解单源最短路径问题的一种常用算法,它可以处理有向图或者无向图,边权可以是正数、负数,但是不能有负环。

二、SPFA 算法

1、SPFA算法的基本流程

1. 初始化

首先我们需要起点s到其他顶点的距离初始化为一个很大的值(比如9999999,像是 JAVA 中可以设置 Integer.MAX_VALUE 来使),并将起点s的距离初始化为0。同时,我们还需要将起点s入队。

请添加图片描述

2. 迭代

每次从队列中取出一个顶点u,遍历所有从u出发的边,对于边(u,v)(其中v为从u可以到达的顶点),如果s->u->v的路径长度小于s->v的路径长度,那么我们就更新s->v的路径长度,并将v入队。

请添加图片描述

3. 循环

不断进行步骤2,直到队列为空。

4. 判断

最后,我们可以得到从起点s到各个顶点的最短路径长度,如果存在无穷小的距离,则说明从起点s无法到达该顶点。

其次,需要注意的是,SPFA算法中存在负环问题。如果存在负环,则算法会陷入死循环。因此,我们需要添加一个计数器,记录每个点进队列的次数。当一个点进队列的次数超过图中节点个数时,就可以判定存在负环。

2、代码详解

以下是使用Java实现 SPFA算法的代码,其中Graph类表示有向图或无向图,Vertex类表示图中的一个顶点,Edge类表示图中的一条边。

import java.util.*;

class Graph {   // 图
    private List<Vertex> vertices;  // 顶点集

    public Graph() {
        vertices = new ArrayList<Vertex>();
    }

    public void addVertex(Vertex v) {   // 添加顶点
        vertices.add(v);
    }   // 添加顶点

    public List<Vertex> getVertices() { // 返回顶点
        return vertices;
    }   // 获取顶点集
}

class Vertex {  // 点
    private int id; // 点 id
    private List<Edge> edges;   // 连接到该顶点的边
    private int distance;   // 从源顶点到该顶点的最短距离,MAX_VALUE init
    private boolean visited;    // 在图的遍历过程中是否访问过该顶点,false init

    public Vertex(int id) {
        this.id = id;
        edges = new ArrayList<Edge>();
        distance = Integer.MAX_VALUE;
        visited = false;
    }

    public int getId() {    // 获取 id
        return id;
    }

    public void addEdge(Edge e) {   // 将连接到该顶点边添加到列表中
        edges.add(e);
    }   // 添加图到边

    public List<Edge> getEdges() {  // 获取连接到该顶点的边集
        return edges;
    }   // 获取图中边

    public int getDistance() {  // 获取从源顶点到该顶点的最短距离
        return distance;
    }   // 获取源顶点到该顶点的最短距离

    public void setDistance(int distance) { //设置最短距离
        this.distance = distance;
    }   // 设置源顶点到该顶点的最短距离

    public boolean isVisited() {    // 获取在图的遍历过程中是否访问过该点
        return visited;
    }   // 获取图遍历过程中是否访问过该点

    public void setVisited(boolean visited) {   // 设置在图的遍历过程中是否访问过该点
        this.visited = visited;
    }   // 设置图遍历过程中是否访问过该点
}

class Edge {    // 边
    private Vertex source;  // 源顶点
    private Vertex destination; // 目标顶点
    private int weight; // 边的权重

    public Edge(Vertex source, Vertex destination, int weight) {
        this.source = source;
        this.destination = destination;
        this.weight = weight;
    }

    public Vertex getSource() { // 返回源顶点
        return source;
    }   // 获取源点

    public Vertex getDestination() {    // 返回目标顶点
        return destination;
    }   // 获取目标顶点

    public int getWeight() {    // 获取边的权重
        return weight;
    }   // 获取边的权重
}

// SPFA 算法
public class SPFA { 
    public static void spfa(Graph graph, Vertex source) {
        // 初始化
        Queue<Vertex> queue = new LinkedList<Vertex>(); // 初始化一个顶点队列,使用该队列来跟中需要处理的顶点 
        for (Vertex v : graph.getVertices()) {  // 初始化最短距离和是否访问过该点
            v.setDistance(Integer.MAX_VALUE);
            v.setVisited(false);
        }

        source.setDistance(0); // 将源顶点到自身的最短距离设为0
        queue.add(source);  // 将源顶点添加到队列中

        // 迭代
        int count = 0;  // 用于检测图中的负环,count超过图中顶点的总数,抛出异常

        // 查找从一个源顶点到图中所有其它顶点的最短路径
        while (!queue.isEmpty()) {  
            Vertex u = queue.poll();    // 存储SPFA算法正在处理的顶点,poll 方法将下一个顶点从队列中取出
            u.setVisited(false);    // 标记该顶点为未访问,以便在算法中再次对其处理
            
            // 查找部分,循环遍历当前顶点 u 的所有边
            for (Edge e : u.getEdges()) {   
                Vertex v = e.getDestination();  // 返回边 e 的目标顶点给 v
                int distance = u.getDistance() + e.getWeight(); // 计算源顶点到目标顶点的距离
                if (distance < v.getDistance()) {
                    v.setDistance(distance);    // 更新最短距离
                    if (!v.isVisited()) {   // 如果该顶点未被访问过
                        queue.add(v);   // 将该顶点添加到队列中
                        v.setVisited(true); // 标记该顶点已被访问
                        count++;    // 负环 + 1
                        if (count > graph.getVertices().size()) {   // 检查 SPFA 算法处理的顶点数是否大于图中顶点总数
                            throw new RuntimeException("Negative cycle detected");
                        }
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        
        // 构造图
        Graph graph = new Graph();
        // 构造顶点
        Vertex s = new Vertex(0);
        Vertex a = new Vertex(1);
        Vertex b = new Vertex(2);
        Vertex c = new Vertex(3);
        Vertex d = new Vertex(4);
        // 点加边
        s.addEdge(new Edge(s, a, 2));
        s.addEdge(new Edge(s, c, 1));
        a.addEdge(new Edge(a, b, 3));
        b.addEdge(new Edge(b, d, 2));
        c.addEdge(new Edge(c, d, 1));
        // 边加点
        graph.addVertex(s);
        graph.addVertex(a);
        graph.addVertex(b);
        graph.addVertex(c);
        graph.addVertex(d);

        // 调用SPFA算法求解最短路径
        spfa(graph, s);

        // 输出结果
        for (Vertex v :graph.getVertices()) {
            System.out.println("Shortest distance from source to vertex " + v.getId() + " is " + v.getDistance()); 
        } 
    } 
}

上面的代码实现了SPFA算法,并计算了从给定源点到图中其他所有顶点的最短路径。主要思路如下:

  1. 初始化:将所有顶点的距离设置为正无穷,将源点的距离设置为0,将源点加入队列。
  2. 迭代:从队列中取出一个顶点u,遍历它的所有邻居v。如果u到源点的距离加上u到v的边的权重小于v的距离,则更新v的距离,并将v加入队列中。如果v已经在队列中,则不需要再次添加。
  3. 如果队列为空,则算法结束。如果队列非空,则回到步骤2。

SPFA算法的时间复杂度取决于负权边的数量。如果图中没有负权边,算法的时间复杂度是O(E),其中E是边的数量。但是如果图中有负权边,算法的时间复杂度将达到O(VE),其中V是顶点的数量,E是边的数量。因此,为了避免算法的时间复杂度变得非常高,应尽可能避免在图中使用负权边。

三、SPFA 算法已死 ?

这个问题引发了很多OI选手和出题人的讨论,虽然 SPFA 算法在实际应用中具有一定的优势,但它也有一些缺点,导致它被称为"已死"的算法之一。以下是几个原因:

  • 可能会进入负环:SPFA 算法可以处理负权边,但是如果有负权环,算法将无法结束,因为每次都会沿着负权环一遍一遍地更新距离,导致算法陷入死循环。

  • 时间复杂度不稳定:在最坏情况下,SPFA 算法的时间复杂度可以达到 O ( V E ) O(VE) O(VE),其中 V V V E E E 分别是图中的顶点数和边数。而在最好情况下,时间复杂度只有 O ( E ) O(E) O(E)。因此,SPFA 算法的时间复杂度是不稳定的。

  • 存在更好的算法:对于单源最短路径问题,已经有更好的算法出现,如 Dijkstra 算法和 Bellman-Ford 算法。这些算法在时间复杂度和稳定性方面都比 SPFA 算法更优秀。

虽然 SPFA 算法在某些情况下可以发挥出优势,但是它的缺点也是无法忽视的,而且已经有更好的算法出现,不少大佬也或多或少的对 SPFA 算法进行了优化,更多优化的内容以及最短路径算法可以在论文中找到。因此,SPFA 算法已经不是首选算法,也可以说是已经“死亡”了。

在这里插入图片描述

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

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

相关文章

PySide2 QWebEngine与Web js交互

文章目录 单向交互双向传值案例 单向交互 QWebEngineView加载web页面&#xff0c;web页面中点击按钮&#xff0c;执行js代码&#xff0c;js的返回值传给QWebEnginePage&#xff0c;使用python进行保存结果。 单向&#xff0c;js向python(PySide2)端传输数据。 前端实现 <…

前端web3入门脚本三:一键完成与dex的交互,羊毛党必备

前言 该脚本用途&#xff1a;一键可以完成与dex的所有交互&#xff0c;包括2次swap&#xff0c;添加/移除流动性&#xff0c;以及farm和提取LP。一次运行可以有6条交易记录。 无论是个人单刷还是羊毛党批量地址刷交互都完美适配。当然反女巫方案不在这次文章的讨论范围内。 一、…

javascript中find(), filter(), some(), every(), map()等方法介绍

1、find() find() 用于找出第一个符合条件的数组成员。它的参数是一个回调函数&#xff0c;所有数组成员依次执行该回调函数&#xff0c;直到找出第一个返回值为true的成员&#xff0c;然后返回该成员。如果没有符合条件的成员&#xff0c;则返回undefined。 find()方法的回调…

利用Matab进行覆盖计算----战术计算

在 contour函数中添加如下代码 %------- 计算畅通区面积和占比例 --------% Spi*maxrange*maxrange/1e6; S0 nnz(isInRange)*reslons*reslats/1e6;isnn ~isnan(cdata); cdata0 cdata(isnn); S1numel(cdata0)*reslons*reslats/1e6;AS1/S0; % 畅通区所占比例; fprintf("…

CLion开发工具 | 06 - 使用CLion开发STM32(无需Cmake)

专栏介绍 文章目录 专栏介绍一、准备工作1. 工具准备2. 裸机工程准备二、使用CLion打开工程三、基于CLion写代码1. LED blink代码2. printf重定位代码四、编译工程1. 编译配置2. 选择编译目标3. 编译五、烧录1. OpenOCD基础知识(了解)2. 设置CLion路径3. 新建CLion配置文件4.…

面试总结,4年经验

小伙伴你好&#xff0c;我是田哥。 本文内容是一位星球朋友昨天面试遇到的问题&#xff0c;我把核心的问题整理出来了。 1&#xff1a;Java 层面的锁有用过吗&#xff1f;除了分布式锁以外 是的&#xff0c;Java中提供了多种锁机制来保证并发访问数据的安全性和一致性。常见的J…

分析GC日志解读

目录 GC分类 GC日志分类 GC日志结构剖析 透过日志看垃圾收集器 透过日志看GC原因 GC日志分析工具 GC分类 针对HotSpot VM的实现&#xff0c;它里面的GC按照回收区域又分为两大种类型&#xff1a;一种是部分收集&#xff08;Partial GC&#xff09;&#xff0c;一种是整堆…

VPN 虚拟专用网络隧道

1 什么是VPN VPN(全称&#xff1a;Virtual Private Network)虚拟专用网络&#xff0c;是依靠ISP和其他的NSP&#xff0c;在公共网络中建立专用的数据通信的网络技术&#xff0c;可以为企业之间或者个人与企业之间提供安全的数据传输隧道服务。在VPN中任意两点之间的链接并没有…

从零开始学习Linux运维,成为IT领域翘楚(二)

文章目录 &#x1f525;Linux系统目录结构&#x1f525;Linux用户和用户组&#x1f525;Linux用户管理 &#x1f525;Linux系统目录结构 文件系统组织结构 ⭐ /lib 系统开机所需要最基本的动态链接共享库&#xff0c;其作用类似于Windows里的DLL文件。 几乎所有的应用程序都需…

PACS系统源码,大型医院PACS源码集成三维重建

PACS系统为医院提供包括放射、超声、核医学、病理、内窥镜、心电图室在内的所有影像检查数字化的一体化解决方案。 它涵盖了传统PACS和RIS系统的所有功能&#xff0c;以构建全数字化影像科为目标&#xff0c;致力于实现对医院所有影像数据的统一管理、影像检查工作流的自动化&a…

POJ3704 括号匹配问题 递归方法

目录 题目 算法 完整代码 题目 参考 递归: https://blog.csdn.net/qq_45272251/article/details/103257953 利用了递归, 但思路稍复杂了 循环: https://blog.csdn.net/weixin_50340097/article/details/114579805 (看起来是递归其实是循环. 每次递归其实是循环内一次迭…

牛客网Python入门103题练习|【07--循环语句(2)】

⭐NP55 2的次方数 描述 在Python中&#xff0c; * 代表乘法运算&#xff0c; ** 代表次方运算。 请创建一个空列表my_list&#xff0c;使用for循环、range()函数和append()函数令列表my_list包含底数2的 [1, 10] 次方&#xff0c;再使用一个 for 循环将这些次方数都打印出来…

【Linux问题合集001】Linux中如何将用户添加到sudo组中的步骤

看教程的前提我的linux当前用户是zhou&#xff0c;看以下步骤时将zhou看做你的liunx当前用户就行了&#xff1a; 一、 以root用户登录到系统。 在Linux系统中&#xff0c;root用户是具有完全系统管理权限的超级用户。要以root用户身份登录到系统&#xff0c;您可>以使用以下…

继续打脸水货教程:关于可变对象与不可变对象

入门教程、案例源码、学习资料、读者群 请访问&#xff1a; python666.cn 大家好&#xff0c;欢迎来到 Crossin的编程教室 &#xff01; 今天这篇我要继续来打脸互联网上各种以讹传讹的水货教程。 前阵子我们聊了下Python中有关函数参数传递以及变量赋值的一些内容&#xff1a;…

LeetCode0014.最长公共前缀 Go语言AC笔记

时间复杂度&#xff1a;O(n) 解题思路 纵向扫描法。先扫描所有字符串的第一个字符&#xff0c;如果都相同就再次扫描所有字符串的第二个字符&#xff0c;直到某一字符串被扫描完或者出现了不相同的字符&#xff0c;此时就返回该字符串该字符的前缀。 为了确定所有字符是否相同…

【flask】三种路由和各自的比较配置文件所有的字母必须大写if __name__的作用核心对象循环引用的几种解决方式--难

三种路由 方法1&#xff1a;装饰器 python C#, java 都可以用这种方式 from flask import Flask app Flask(__name__)app.route(/hello) def hello():return Hello world!app.run(debugTrue)方法2: 注册路由 php python from flask import Flask app Flask(__name__)//app…

Java IO流第一章

Java IO流第一章 &#xff08;一&#xff09;简介 本文主要是从最基础的BIO式通信开始介绍到NIO , AIO&#xff0c;读者可以清晰的了解到阻塞、同步、异步的现象、概念和特征以及优缺点。 通信技术整体解决的问题 局域网内的通信要求。多系统间的底层消息传递机制。高并发下…

如何自制云平台,并实现远程访问控制?

除了阿里、腾讯各种云&#xff0c;计算机大神们都想自己搭建IoT云平台。今天小编跟大家分享一种用UbuntuEMQXNode-RED方式自制IoT云平台的方法&#xff0c;并实现无公网IP随时访问远程数据&#xff01; 第一步 Step1搭建EMQX服务器 1.搭建IoT平台需要一个服务器&#xff0c;这…

windows安装rocketmq

windows安装rocketmq 问题背景操作步骤Lyric&#xff1a; 请再给我 一个理由 问题背景 最近有使用rocketmq&#xff0c;为测试方便&#xff0c;在本地安装rocketmq 注意事项&#xff1a; 默认已安装java1.8&#xff0c;启动mq必须是1.8版本&#xff0c;我之前使用11版本&…

命令行 控制 易微联 wifi通断器

有个设备需要远程控制开关&#xff0c;最简单的方式就是通过一直在线运行的 Pi&#xff0c;进行命令行控制智能开关。 1、材料准备 找个最便宜的智能开关&#xff0c;话说易微联的做的真是便宜&#xff0c;销售量也很大。 这种 网上叫 Wifi通断器&#xff0c;或者智能开关&a…