搜索与图论 - spfa 算法

news2024/11/16 9:55:09

文章目录

  • 一、spfa 算法
    • 1. spfa 算法简介
    • 2. spfa 算法和 bellman-ford 算法的区别
    • 3. spfa 算法和 dijkstra 算法的区别
    • 4. spfa 算法实现步骤
    • 5. spfa 算法举例图解
    • 6. spfa 算法用于求最短路和判断负环,详见下面两道例题。
  • 二、spfa 算法例题—— spfa 求最短路
    • 具体实现
      • 1. 样例演示
      • 2. 实现思路
      • 3. 代码注解
      • 4. 实现代码
  • 三、spfa 算法例题—— spfa 判断负环
    • 具体实现
      • 1. 实现思路
      • 2. 代码注解
      • 3. 实现代码

一、spfa 算法

1. spfa 算法简介

  • spfa 算法是 bellman-ford 算法的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环。spfa 最坏情况下时间复杂度和朴素 bellman-ford 相同,为 O(nm)。
  • 在这里,我们需要明确一下 松弛 的概念:
  • 节点 u 以及它的邻节点 v,从起点跑到邻节点 v 有好多跑法,有的跑法经过 u ,有的不经过。
  • 经过节点 u 的跑法的距离就是 dist[u] + 节点 u 到邻节点 v 的距离。
  • 松弛操作,就是看一看 dist[v] 和 dist[u] + 节点 u 到邻节点 v 的距离哪个大一点。
  • 如果前者大一点,就说明当前的不是最短路,就要赋值为后者,这就叫做松弛

2. spfa 算法和 bellman-ford 算法的区别

  • bellman-ford 算法具体讲解详见搜索与图论 - bellman-ford 算法。
  • (1)bellman-ford 算法中,循环 n 次,每次遍历 m 条边,每次遍历的时候,把入度的点的距离更新成最小。但是,这样就循环遍历了很多用不到的边。比如第一次遍历,只有第一个点的临边是有效的。
  • (2) 因此,spfa 算法中,采用邻接表的方式,只用到有效的点(更新了临边的点),直到每个点都是最短距离为止。采用队列优化的方式存储每次更新了的点,每条边最多遍历一次。如果存在负权回路,从起点 1 出发,回到 1 距离会变小, 会一直在三个点中循环。
  • 因此,便会产生一个疑问,我们不用队列,直接遍历所有的点可以吗?
  • 这样操作似乎不行,因为是更新了点之后,这个点的邻边才可以用,如果没有更新到循环的点,那么循环的点也是不可用的。

3. spfa 算法和 dijkstra 算法的区别

  • dijkstra 算法具体讲解详见搜索与图论 - dijkstra 算法。
  • (1)在 spfa 算法当中,st 数组用来检验队列中是否有重复的点。
  • spfa 算法从队列中使用了当前的点,会把该点 pop 掉,状态数组 st[i] = false (说明堆中不存在了) ,更新邻边之后,把邻边放入队列中, 并且设置状态数组为 true,表示放入队列中 。如果当前的点距离变小,可能会再次进入队列,因此可以检验负环。
  • 每次更新可以记录一次,如果记录的次数 > n,代表存在负环(环一定是负的,因为只有负环才会不断循环下去)。
  • (2) 在 dijkstra 算法当中,st是一个集合,不是检验队列中的点。
  • dijkstra 算法使用当前点更新邻边之后,把该点加入到一个集合中,使用该点更新邻边,并把邻边节点和距离起点的距离置入堆中(不设置状态数组)。下一次从堆中取最小值,并把对应的节点放入集合中,继续更新邻边节点,直到所有的点都存入集合中。因此 dijkstra 算法不判断负环。
  • 从上述描述中能看出,dijkstra 算法存放节点的堆,具有单调性,而 spfa 算法的队列不需要具有单调性。
算法名称对应问题
dijkstra 算法只能处理带正权边的图
bellman-ford 算法可以处理任意带负权边和负权环的图
spfa 算法可以处理带负权边的图

4. spfa 算法实现步骤

  • (1) 建立一个队列,初始时队列里只有起始点。
  • (2) 建立一个数组记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为 0)。
  • (3) 建立一个数组,标记点是否在队列中。
  • (4) 队头不断出队,计算始点起点经过队头到其他点的距离是否变短,如果变短且被点不在队列中,则把该点加入到队尾。
  • (5) 重复执行直到队列为空。
  • (6) 在保存最短路径的数组中,就得到了最短路径。

5. spfa 算法举例图解

  • 给定一个有向图,如下,求 A~E 的最短路。

在这里插入图片描述

  • 节点 A 首先入队,然后节点 A 出队,计算出到节点 B 和节点 C 的距离会变短,更新距离数组,节点 B 和节点 C 没在队列中,节点 B 和节点 C 入队。

在这里插入图片描述

  • 节点 B 出队,计算出到节点 D 的距离变短,更新距离数组,节点 D 没在队列中,节点 D 入队。然后节点 C 出队,无点可更新。

在这里插入图片描述

  • 节点 D 出队,计算出到节点 E 的距离变短,更新距离数组,节点 E 没在队列中,节点 E 入队。

在这里插入图片描述

  • 节点 E 出队,此时队列为空,源点到所有点的最短路已被找到,最短路即为 8。

在这里插入图片描述

6. spfa 算法用于求最短路和判断负环,详见下面两道例题。

二、spfa 算法例题—— spfa 求最短路

题目描述

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible
数据保证不存在负权回路。

输入格式

第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式

输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 impossible

数据范围

1 ≤ n,m ≤ 1e5
图中涉及边长绝对值均不超过 10000。

输入样例

3 3
1 2 5
2 3 -3
1 3 4

输出样例

2

具体实现

1. 样例演示

  • 输入 n = 3,m = 3,表示求从 1 号点到 n = 3 号点的最短距离,共有 m = 3 条边。
  • 从 1 号点到 2 号点的边长为 5 。
  • 从 2 号点到 3 号点的边长为 -3 。
  • 从 1 号点到 3 号点的边长为 4 。
  • 显然,最短路径是 2 。

2. 实现思路

  • 详见 spfa 算法举例图解。

3. 代码注解

  • int h[N], w[N], e[N], ne[N], idx;使用邻接表来存储图。
  • int dist[N];保存最短路径的值。
  • int q[N], hh, tt = -1;表示队列。
  • memset(h, -1, sizeof h);初始化邻接表。
  • memset(dist, 0x3f, sizeof dist);初始化距离。
  • 其他代码已经标记在实现代码当中。

4. 实现代码

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;
int h[N], e[N], w[N], ne[N], idx;//邻接表,存储图
int st[N];//标记顶点是不是在队列中
int dist[N];//保存最短路径的值
int q[N], hh, tt = -1;//队列

//图中添加边和边的端点
void add(int a, int b, int c)
{
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx;
    idx++;
}

void spfa()
{
    tt++;
    q[tt] = 1;//从1号顶点开始松弛,1号顶点入队
    dist[1] = 0;//1号到1号的距离为 0
    st[1] = 1;//1号顶点在队列中
    
    //不断进行松弛
    while(tt >= hh)
    {
        int a = q[hh];//取对头记作a,进行松弛
        hh++;
        
        st[a] = 0;//取完队头后,a不在队列中了
        
        for(int i = h[a]; i != -1; i = ne[i])//遍历所有和a相连的点
        {
            //获得和a相连的点和边
            int b = e[i], c = w[i];
            
            //如果可以距离变得更短,则更新距离
            if(dist[b] > dist[a] + c)
            {
                //更新距离
                dist[b] = dist[a] + c;
                
                //如果没在队列中
                if(!st[b])
                {
                    tt++;
                    q[tt] = b;//入队
                    st[b] = 1;//打标记
                }
            }
        }
    }
}

int main()
{
    memset(h, -1, sizeof h);//初始化邻接表
    memset(dist, 0x3f, sizeof dist);//初始化距离
    
    int n, m;//保存点的数量和边的数量
    cin >> n >> m;
    
    //读入每条边和边的端点
    for(int i = 0; i < m; i++)
    {
        int a, b, w;
        cin >> a >> b >> w;
        
        //加入到邻接表
        add(a, b, w);
    }
    
    spfa();
    
    if(dist[n] == 0x3f3f3f3f )//如果到n点的距离是无穷,则不能到达 
    {
        cout << "impossible";
    }
    else 
    {
        cout << dist[n];//否则能到达,输出距离
    }
    system("pause");
    return 0;
}

三、spfa 算法例题—— spfa 判断负环

题目描述

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数
请你判断图中是否存在负权回路。

输入格式

第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式

如果图中存在负权回路,则输出 Yes,否则输出 No

数据范围

1 ≤ n ≤ 2000
1 ≤ m ≤ 10000
图中涉及边长绝对值均不超过 10000。

输入样例

3 3
1 2 -1
2 3 4
3 1 -4

输出样例

Yes

具体实现

1. 实现思路

  • 判断负环的方法和 bellman-ford 算法相同,应用抽屉原理。
  • 抽屉原理: 如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。
  • 如果一个点在被入队次数大于 n 次,那么说明存在负环。
  • 原理是虽然一个点在状态数组会被多次更新,但是它的更新次数不会大于 n-1 次,因为从一个点到另一个点最多经过 n-1 条边。如果存在负环则会造成无限入队的情况,spfa 算法陷入死循环,这时候就可直接退出了。
  • 个人理解:如果某一个点的 cnt >= n 的话说明这个点还没到最后一个点的时候就已经有了 n 条边了,早就已经符合出现负环的情况了。
  • (1) dist[x] 记录虚拟源点到 x 的最短距离。
  • (2) cnt[x] 记录当前 x 点到虚拟源点最短路的边数,初始每个点到虚拟源点的距离为 0 ,只要他能再走 n 步,即 cnt[x] >= n,则表示该图中一定存在负环,由于从虚拟源点到 x 至少经过 n 条边时,则说明图中至少有 n + 1 个点,表示一定有点是重复使用。
  • (3) 若 dist[j] > dist[t] + w[i],则表示从 t 点走到 j 点能够让权值变少,因此进行对该点 j 进行更新,并且对应 cnt[j] = cnt[t] + 1,往前走一步。

2. 代码注解

  • int dist[N], cnt[N];记录每个点到起点的边数,当 cnt[i] >= n 表示出现了边数 >= 结点数,必然有环,而且一定是负环。
  • bool st[N];判断当前的点是否已经加入到队列当中了;已经加入队列的结点就不需要反复的把该点加入到队列中了,就算此次还是会更新到起点的距离,那只用更新一下数值而不用加入到队列当中,意味着,st数组起着提高效率的作用,不在乎效率的话,去掉也可以。
  • 其他代码注解已经标记在实现代码当中。

3. 实现代码

#include <bits/stdc++.h>
using namespace std;

const int N = 2010, M = 10010;

int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];

void add(int a, int b, int c)
{
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx;
    idx ++ ;
}

bool spfa()
{
    queue<int> q;

    for (int i = 1; i <= n; i ++ )
    {
        st[i] = true;
        q.push(i);
    }
    
    //队列中的点用来更新其他点到起点的距离
    while (q.size())
    {
        int t = q.front();
        q.pop();
        
        //t出队,标记出队
        st[t] = false;
        
        //更新与t邻接的边
        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                //结点j可以通过中间点t降低距离
                dist[j] = dist[t] + w[i];
                
                //那么结点j在中间点t的基础上加一条到自己的边
                cnt[j] = cnt[t] + 1;
                
                //边数不小于结点数,出现负环,函数结束
                if (cnt[j] >= n) 
                {
                    return true;
                }
                
                //若此时j没在队列中,则进队。
                //已经在队列中了,上面已经更新了数值。重复加入队列降低效率
                if (!st[j])
                {
                    //j进队,标记进队
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    
    //走到这了,函数还没结束,意味着边数一直小于结点数,不存在负环
    return false;
}

int main()
{
    cin >> n >> m;

    memset(h, -1, sizeof h);

    while (m -- )
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }

    if (spfa())
    {
        puts("Yes");
    }
    else 
    {
        puts("No");
    }
    system("pause");
    return 0;
}

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

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

相关文章

1. SpringBoot 整合 Canal

勿以恶小而为之&#xff0c;勿以善小而不为----- 刘备 SpringBoot 整合 Canal pom.xml 添加 canal.client 依赖 (1.1.5 改动很大&#xff0c;这儿客户端用 1.1.4) <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.a…

【云原生进阶之容器】第一章Docker核心技术1.5.3节——cgroups数据结构剖析

1 cgroups数据结构解析 从进程的角度出发来剖析 cgroups 相关数据结构之间的关系。在 Linux 中管理进程的数据结构是 task_struct。cgroup表示进程的行为控制,因为子系统必须要知道进程是位于哪一个cgroup,所以在struct task_struct和cgroup中存在一种映射。 1.1 task_struc…

【数据结构进阶】二叉平衡树

一、 二叉平衡树 概念 二叉搜索树有称 二叉排序树&#xff0c;它也可以是一个空树。 如果它的左子树不为空&#xff0c;则左子树上所有结点的值都小于根结点的值如果他的右子树不为空&#xff0c;则右子树上所有结点的值都大于根结点的值它的左右子树也分别是二叉搜索树 由…

【Acwing 周赛#82】AcWing 4783. 多米诺骨牌

目录 4782.第k个数 4783. 多米诺骨牌 - bfs 记录时间 4782.第k个数 java大顶堆 import java.util.*;public class Main {public static void main(String[] args ){Scanner scnew Scanner(System.in);int nsc.nextInt(),ksc.nextInt();k--;PriorityQueue<Integer> qn…

HarmonyOS玩转ArkUI动效 - 水母动画

前言 本文会详细讲解我参加&#xff1a; HarmonyOS【挑战赛第三期】玩转ArkUI动效的项目 我的参赛项目源码&#xff1a;【挑战赛第三期】JellyfishAnimation 动画效果参考自&#xff1a;cassie-codes的水母SVG 华为鸿蒙已经放弃Java作为鸿蒙的开发语言&#xff0c;开发了一个申…

基于java+springmvc+mybatis+vue+mysql的校运会管理系统小程序

项目介绍 运动是伴随人类一生的一种行为和活动&#xff0c;只有不断的运动才能够彰显生命的意义&#xff0c;尤其是当代的学生&#xff0c;课业繁重往往忽略了体育锻炼&#xff0c;为了能够提高学子们对体育运动的积极性&#xff0c;基本所有的高校每年都会定期的举办运动会。…

软件设计师常考知识点

絮絮叨叨&#xff1a;哈喽大家好&#xff5e;这里是一口八宝周[送花花]。前段时间闲来无事报考了今年的软件设计师考试&#xff0c;觉得凭借自己“自律”的学习&#xff0c;一定可以把书看完&#xff0c;把题刷完顺利上岸&#x1f60e;。 书确实没看完&#xff0c;但是视频学完…

MySQL or条件命中

需求如下&#xff1a;当写入SQL语句中有任意一个字段在数据库中存在时&#xff0c;不可写入&#xff0c;并返回具体的重复字段。 使用Java Steam处理数据集循环执行SQL需要多次执行SQL&#xff0c;适合单条件索引的情况下使用&#xff0c;现状是想执行少量的SQL实现需求&#…

操作系统,计算机网络,数据库刷题笔记12

操作系统&#xff0c;计算机网络&#xff0c;数据库刷题笔记12 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xf…

u盘/移动硬盘的视频文件出现损坏怎么办?修复损坏视频办法分享!

一般情况下&#xff0c;视频文件都是比较大&#xff0c;如果直接存放于电脑&#xff0c;就会占用比较大的存储空间。不少小伙伴都会把它存放于U盘或者移动硬盘&#xff0c;而且作为一种便携式硬盘&#xff0c;可以在各电脑之间使用&#xff0c;非常方便。但这也造成文件很容易出…

【OpenCV-Python】教程:6-4 Depth Map from Stereo Images 立体图像的深度图

OpenCV Python Depth Map from Stereo Images 立体图像的深度图 【目标】 通过立体图像创建一个深度图 【理论】 上一节中&#xff0c;我们学习了一些基本概念&#xff0c;如对极约束和其他一些相关术语。我们还可以看到&#xff0c;如果我们有同一个场景的两张图像&#x…

无尘室中高效过滤器的更换时间

广州特耐苏净化设备有限公司详细介绍无尘室中高效过滤器使用多久需要更换 为了保证产品的生产质量和人员工作环境的舒适性&#xff0c;无尘室对环境的湿度、温度、新鲜空气量、状态、照明等都有严格的规定。无尘室系统一般配备三级过滤器的空气净化系统&#xff0c;采用初效、…

西门子博途与上位机TCPIP通信

1、PLC硬件IP设定及组态如下图&#xff1a; 堆垛机 1号机 IP地址&#xff1a;190.20.0.72 掩码 255.255.255.0 2、PLC与上位机TCP网络连接组态如下图&#xff1a; WCS上位机IP地址设定 IP地址&#xff1a;190.20.0.&#xff12;&#xff15;&#xff10; 掩码 255.255.255.0…

[强网杯 2019]Upload

一、信息收集 打开界面是一个登陆&#xff0c;界面随便注册一个然后登陆 然后是一个上传文件的操作&#xff0c;点击php后发现的回显 然后又上传了一个图片文件&#xff0c;显示出了照片的存放路径&#xff0c;这里可以上传图片码 然后抓包看一下&#xff0c;base64解码 a:5:{…

想要学习次世代3d建模,需要用到哪些软件,制作流程是什么?

在校大学生想要学习游戏次世代建模&#xff0c;首先就要先了解次世代是什么&#xff1f;制作建模所需要用到的软件有哪些&#xff0c;随着次世代游戏的不断发展&#xff0c;游戏美术制作流程也迎来了全新的制作方式&#xff0c;像ZBrush、SP等软件就解放了我们的双手和制作方式…

Spark-Spark Sql(DataFrame、DataSet、Scala代码开发、数据的加载和保存)

文章目录Spark SqlHive and SparkSQL特点DataFrame 是什么DataSet 是什么核心编程新的起点DataFrame创建SQL语法DSL 语法RDD > DataFrameDataFrame > RDDDataSet创建RDD > DataSetDataSet > RDDDataFrame > DataSetDataSet > DataFrameRDD、DataFrame、DataS…

CSND近期推出的猿如意到底有没有必要安装

可能很多人还不知道猿如意是什么&#xff0c;先给大家科普一下 猿如意 工具代码&#xff0c;一搜就有 程序员的如意兵器 猿如意是一款面向开发者的辅助开发工具箱&#xff0c;包含了效率工具、开发工具下载&#xff0c;教程文档&#xff0c;代码片段搜索&#xff0c;全网搜索等…

Raydium被盗造成巨额损失,但Zebec Protocol以及$ZBC并未受影响

在12月17日&#xff0c;Solana上最大的DEX Raydium因木马攻击导致流动性资金池所有者帐户的私钥泄露&#xff0c;攻击者访问了资金池所有者帐户&#xff0c;然后能够调用withdraw pnl函数&#xff0c;该函数用于收集池中掉期所赚取的交易/协议费用。 而受影响的资金池包括SOL-…

论文翻译:LiDAR Based Negative Obstacle Detection for Field Autonomous Land Vehicles

论文地址&#xff1a;https://onlinelibrary.wiley.com/doi/full/10.1002/rob.21609?saml_referrer &#xff08;机翻&#xff0c;自己保存观看的&#xff09; Abstract&#xff1a; 野外自动驾驶陆地车辆&#xff08;ALV&#xff09;的负障碍是指沟渠、坑或具有负坡度的地形&…

在Vue3这样子写页面更快更高效

前言 在开发管理后台过程中&#xff0c;一定会遇到不少了增删改查页面&#xff0c;而这些页面的逻辑大多都是相同的&#xff0c;如获取列表数据&#xff0c;分页&#xff0c;筛选功能这些基本功能。而不同的是呈现出来的数据项。还有一些操作按钮。 对于刚开始只有 1&#xff…