搜索与图论 - spfa 算法

简介: 搜索与图论 - spfa 算法

文章目录

一、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 的最短路。


37b57c9feb0f4ef6b724aba6b6151cff.png

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

24764886b0c347e29bda4067f2d882fa.png

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

2019bec6e4e94d96bad481cf6437f499.png

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

3fb644a7e056471bb8586ac9148a96a5.png


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

2feefd78367a4bb3a133a992f59742a9.png

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 算法举例图解。


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;
}































































相关文章
|
19天前
|
算法 搜索推荐 数据库
二分搜索:高效的查找算法
【10月更文挑战第29天】通过对二分搜索的深入研究和应用,我们可以不断挖掘其潜力,为各种复杂问题提供高效的解决方案。相信在未来的科技发展中,二分搜索将继续发挥着重要的作用,为我们的生活和工作带来更多的便利和创新。
31 1
|
1月前
|
算法 决策智能
基于禁忌搜索算法的VRP问题求解matlab仿真,带GUI界面,可设置参数
该程序基于禁忌搜索算法求解车辆路径问题(VRP),使用MATLAB2022a版本实现,并带有GUI界面。用户可通过界面设置参数并查看结果。禁忌搜索算法通过迭代改进当前解,并利用记忆机制避免陷入局部最优。程序包含初始化、定义邻域结构、设置禁忌列表等步骤,最终输出最优路径和相关数据图表。
|
2月前
|
大数据 UED 开发者
实战演练:利用Python的Trie树优化搜索算法,性能飙升不是梦!
在数据密集型应用中,高效搜索算法至关重要。Trie树(前缀树/字典树)通过优化字符串处理和搜索效率成为理想选择。本文通过Python实战演示Trie树构建与应用,显著提升搜索性能。Trie树利用公共前缀减少查询时间,支持快速插入、删除和搜索。以下为简单示例代码,展示如何构建及使用Trie树进行搜索与前缀匹配,适用于自动补全、拼写检查等场景,助力提升应用性能与用户体验。
58 2
|
1月前
|
存储 算法 C++
【搜索算法】 跳马问题(C/C++)
【搜索算法】 跳马问题(C/C++)
|
1月前
|
人工智能 算法 Java
【搜索算法】数字游戏(C/C++)
【搜索算法】数字游戏(C/C++)
|
3月前
|
算法
【算法】递归、搜索与回溯——汉诺塔
【算法】递归、搜索与回溯——汉诺塔
|
3月前
|
算法
【算法】递归、搜索与回溯——简介
【算法】递归、搜索与回溯——简介
|
1月前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
8天前
|
算法 数据安全/隐私保护 索引
OFDM系统PAPR算法的MATLAB仿真,对比SLM,PTS以及CAF,对比不同傅里叶变换长度
本项目展示了在MATLAB 2022a环境下,通过选择映射(SLM)与相位截断星座图(PTS)技术有效降低OFDM系统中PAPR的算法实现。包括无水印的算法运行效果预览、核心程序及详尽的中文注释,附带操作步骤视频,适合研究与教学使用。
|
16天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。
下一篇
无影云桌面