草庐IT

【最短路算法】第三弹:一文学懂spfa 算法(队列优化的Bellman-Ford算法)

是瑶瑶子啦 2023-08-11 原文

  • 博主简介:努力学习的大一在校计算机专业学生,热爱学习和创作。目前在学习和分享:算法、数据结构、Java等相关知识。
  • 博主主页: @是瑶瑶子啦
  • 所属专栏: 算法 ;该专栏专注于蓝桥杯和ACM等算法竞赛🔥
  • 近期目标:写好专栏的每一篇文章

🥂前言

前面,我们分别介绍了Dijkstra算法(【最短路算法】一篇文章彻底弄懂Dijkstra算法|多图解+代码详解)和Bellman-Ford算法
【最短路算法】第二弹:一文弄懂Bellman-Ford(贝尔曼福特算法)
前者用于求单源、正权边最短路问题,后者用于求单源、带负权边最短路问题。通过对Bellman-Ford算法讲解,我们知道,Bellman-Ford算法美中不足的一点在于时间复杂度是O(nm),时间复杂度过高。

今天,我们学习的spfa算法,优化了Bellman-Ford算法,时间复杂度 平均情况下 O(m),最坏情况下 O(nm), n 表示点数,m 表示边数。

目录

🍚一、算法思想

在讲具体算法思想之前,我们来仔细探讨一下,优化的点在哪里?

Bellman-ford算法慢,就慢在,其实在后面遍历时,每次迭代都要遍历所有边。前面我们说到过,迭代次数代表从源点出发,经过不超过迭代次数的边数,所经过的顶点的dist都是准确的,确定的。但是在后面迭代中,又重复去遍历这些边,其实是没有必要的。

因为判断一个点的松驰条件为:假设边为a→bdist[b] = min (dist[b], dist[a]+w),当dist[a](a其实不是某一个点,代表的是从a指向b的边的一群顶点)都一直没有更新,dist[b]当然也不会更新,确定好了dist[b],在后面遍历中,dist[b] = min (dist[b], dist[a]+w)完全没有必要进行。那如何减少这一无意义操作呢?

核心就是,我们只对其dist有更新的顶点进行松驰!(注意,这样一来,spfa当然就不会有bellman-Ford算法的那种迭代次数的意义存在!)

spfa,利用宽搜思想,使用队列为数据结构,优化了这一点。,优化了这一点。

  • 📍首先,将dist变小的即有被更新的点入队(因为它的更新,一般会带来其他点的更新)

  • 📍取出队列元素,并将其弹出

  • 📍对该元素的出边元素进行遍历&松驰

  • 📍如果有元素松驰成功(dist更新,也就是变小了),那么该元素也入队

算法模板如下:(from acWing)

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求最短路

acWing 851. spfa求最短路

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible

数据保证不存在负权回路。
输入格式

第一行包含整数 n 和 m。

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

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 impossible
数据范围
1≤n,m≤105,
图中涉及边长绝对值均不超过 10000。
输入样例:

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

输出样例:

2
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 100010;

int n,m;
int h[N],w[N],e[N],ne[N],idx;//邻接表存储图
int dist[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++;
}

void spfa(){
    memset(dist, 0x3f, sizeof(dist));
    dist[1] = 0;
    
    queue<int> q;//队列
    q.push(1);//因为dist[1] 从0x3f3f3f3f松驰为了0,加入队列
    st[1] = true;
    
    while(q.size()){//while队列不空
        int t = q.front();//取出队头元素,由该点更新其出边点
        
        q.pop();//出队
        
        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]){
                dist[j] = dist[t] +w[i];
                
                if(!st[j]){//对该点做了更新,把该点也要入队
                    q.push(j);
                    st[j] = true;
                }
            }
        }
        
    }
}

int main(){
    scanf("%d%d",&n,&m);
    
    //!h[N]数组要先初始化
    
    memset(h,-1,sizeof(h));
    
    for(int i = 0; i < m; i++){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    
    spfa();
    
    if(dist[n] == 0x3f3f3f3f) puts("impossible");
    else printf("%d\n",dist[n]);
    return 0;
}

🥘三、spfa判断负权回路

  1. spfa判断负环

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你判断图中是否存在负权回路。

输入格式

第一行包含整数 n 和 m。

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

输出格式

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

数据范围

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

🛫思路
统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于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,往前走一步

🛫详细注释题解

#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

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,ne[idx] = h[a],w[idx] = c,h[a] = idx++; 
}

//spfa算法判断负权回路
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();
        
        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;
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}
int main(){
    
    scanf("%d%d",&n,&m);
    
    memset(h,-1,sizeof(h));
    
    while(m --){
        
        int a, b, c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    if(spfa()) puts("Yes");
    else puts("No");
    
    return 0;
}

🛫注意点

  • 为什么不用初始化dist[N]? 因为负环的存在,所以即使每个点距离虚拟原点距离为0,负权回路的存在就可以更新某个点的dist为更小的值.这题是判断,只要有这个变小(松驰)趋势就Ok,并不是求最短路
  • 注意:该题是判断是否存在负环,并非判断是否存在从1开始的负环,因此需要将所有的点都加入队列中,更新周围的点.换句话来说,**从哪个点出发并不重要,重要的是是否有负环!**一定要将所有点入队我认为是因为图可能会不连通,如果负环是在和1这个点不连通的部分(可能图中存在负环,但是从一号点到不了),就无法查到,所以要将所有点都入队!遍历所有点,肯定能遍历到有负环的回路,只要有负环,dist就会更新,顶点就会接着入队,cnt也接着增加!
  • 对比:以 S 点为源点跑 Bellman-Ford 算法时,如果没有给出存在负环的结果,只能说明从 S 点出发不能抵达一个负环,而不能说明图上不存在负环。

有关【最短路算法】第三弹:一文学懂spfa 算法(队列优化的Bellman-Ford算法)的更多相关文章

  1. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

  2. ruby - 尝试比较两个文本文件,并根据信息创建第三个 - 2

    我有两个文本文件,master.txt和926.txt。如果926.txt中有一行不在master.txt中,我想写入一个新文件notinbook.txt。我写了我能想到的最好的东西,但考虑到我是一个糟糕的/新手程序员,它失败了。这是我的东西g=File.new("notinbook.txt","w")File.open("926.txt","r")do|f|while(line=f.gets)x=line.chompifFile.open("master.txt","w")do|h|endwhile(line=h.gets)ifline.chomp!=xputslineendende

  3. 100个python算法超详细讲解:画直线 - 2

    1.问题描述使用Python的turtle(海龟绘图)模块提供的函数绘制直线。2.问题分析一幅复杂的图形通常都可以由点、直线、三角形、矩形、平行四边形、圆、椭圆和圆弧等基本图形组成。其中的三角形、矩形、平行四边形又可以由直线组成,而直线又是由两个点确定的。我们使用Python的turtle模块所提供的函数来绘制直线。在使用之前我们先介绍一下turtle模块的相关知识点。turtle模块提供面向对象和面向过程两种形式的海龟绘图基本组件。面向对象的接口类如下:1)TurtleScreen类:定义图形窗口作为绘图海龟的运动场。它的构造器需要一个tkinter.Canvas或ScrolledCanva

  4. ruby - 在 Ruby 中实现 Luhn 算法 - 2

    我一直在尝试用Ruby实现Luhn算法。我一直在执行以下步骤:该公式根据其包含的校验位验证数字,该校验位通常附加到部分帐号以生成完整帐号。此帐号必须通过以下测试:从最右边的校验位开始向左移动,每第二个数字的值加倍。将乘积的数字(例如,10=1+0=1、14=1+4=5)与原始数字的未加倍数字相加。如果总模10等于0(如果总和以零结尾),则根据Luhn公式该数字有效;否则无效。http://en.wikipedia.org/wiki/Luhn_algorithm这是我想出的:defvalidCreditCard(cardNumber)sum=0nums=cardNumber.to_s.s

  5. Ruby 斐波那契算法 - 2

    下面是我写的一个计算斐波那契数列中的值的方法:deffib(n)ifn==0return0endifn==1return1endifn>=2returnfib(n-1)+(fib(n-2))endend它工作到n=14,但在那之后我收到一条消息说程序响应时间太长(我正在使用repl.it)。有人知道为什么会这样吗? 最佳答案 Naivefibonacci进行了大量的重复计算-在fib(14)fib(4)中计算了很多次。您可以将内存添加到您的算法中以使其更快:deffib(n,memo={})ifn==0||n==1returnnen

  6. Ruby 缺少常量表达式优化? - 2

    我希望Ruby的解析器会进行这种微不足道的优化,但似乎并没有(谈到YARV实现,Ruby1.9.x、2.0.0):require'benchmark'deffib1a,b=0,1whileb由于这两种方法除了在第二种方法中使用预定义常量而不是常量表达式外是相同的,因此Ruby解释器似乎在每个循环中一次又一次地计算幂常数。是否有一些Material说明为什么Ruby根本不进行这种基本优化或只在某些特定情况下进行? 最佳答案 很抱歉给出了另一个答案,但我不想删除或编辑我之前的答案,因为它下面有有趣的讨论。正如JörgWMittag所说,

  7. ruby-on-rails - Rails add_index 算法 : :concurrently still causes database lock up during migration - 2

    为了防止在迁移到生产站点期间出现数据库事务错误,我们遵循了https://github.com/LendingHome/zero_downtime_migrations中列出的建议。(具体由https://robots.thoughtbot.com/how-to-create-postgres-indexes-concurrently-in概述),但在特别大的表上创建索引期间,即使是索引创建的“并发”方法也会锁定表并导致该表上的任何ActiveRecord创建或更新导致各自的事务失败有PG::InFailedSqlTransaction异常。下面是我们运行Rails4.2(使用Acti

  8. ruby-on-rails - 优化读取数据库和写入csv文件 - 2

    我正在尝试从数据库中读取大量单元格(超过100.000个)并将它们写入VPSUbuntu服务器上的csv文件。碰巧服务器没有足够的内存。我正在考虑一次读取5000行并将它们写入文件,然后再读取5000行,等等。我应该如何重构我当前的代码以使内存不会被完全消耗?这是我的代码:defwrite_rows(emails)File.open(file_path,"w+")do|f|f该函数由sidekiqworker调用:write_rows(user.emails)感谢您的帮助! 最佳答案 这里的问题是,当您调用emails.each时,

  9. ruby - 趋势算法 - 2

    我正在开发一个类似微论坛的项目,其中一个特殊用户发布一条快速(接近推文大小)的主题消息,订阅者可以用他们自己的类似大小的消息来响应。直截了当,没有任何形式的“挖掘”或投票,只是每个主题消息的响应按时间顺序排列。但预计会有很高的流量。我们想根据它们引起的响应嗡嗡声来标记主题消息,使用0到10的等级。在谷歌上搜索了一段时间的趋势算法和开源社区应用示例,到目前为止已经收集到两个有趣的引用资料,但我还没有完全理解它们:Understandingalgorithmsformeasuringtrends,关于使用基线趋势算法比较维基百科页面浏览量的讨论,在SO上。TheBritneySpearsP

  10. Ruby - 不支持的密码算法 (AES-256-GCM) - 2

    我收到错误:unsupportedcipheralgorithm(AES-256-GCM)(RuntimeError)但我似乎具备所有要求:ruby版本:$ruby--versionruby2.1.2p95OpenSSL会列出gcm:$opensslenc-help2>&1|grepgcm-aes-128-ecb-aes-128-gcm-aes-128-ofb-aes-192-ecb-aes-192-gcm-aes-192-ofb-aes-256-ecb-aes-256-gcm-aes-256-ofbRuby解释器:$irb2.1.2:001>require'openssl';puts

随机推荐