搜索与图论 - 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;
}































































相关文章
|
29天前
|
算法
【算法】二分算法——搜索插入位置
【算法】二分算法——搜索插入位置
|
1月前
|
机器学习/深度学习 算法 文件存储
【博士每天一篇文献-算法】 PNN网络启发的神经网络结构搜索算法Progressive neural architecture search
本文提出了一种名为渐进式神经架构搜索(Progressive Neural Architecture Search, PNAS)的方法,它使用顺序模型优化策略和替代模型来逐步搜索并优化卷积神经网络结构,从而提高了搜索效率并减少了训练成本。
31 9
|
29天前
|
算法
【算法】递归、搜索与回溯——汉诺塔
【算法】递归、搜索与回溯——汉诺塔
|
1月前
|
存储 算法 调度
基于和声搜索算法(Harmony Search,HS)的机器设备工作最优调度方案求解matlab仿真
通过和声搜索算法(HS)实现多机器并行工作调度,以最小化任务完成时间。在MATLAB2022a环境下,不仅输出了工作调度甘特图,还展示了算法适应度值的收敛曲线。HS算法模拟音乐家即兴创作过程,随机生成初始解(和声库),并通过选择、微调生成新解,不断迭代直至获得最优调度方案。参数包括和声库大小、记忆考虑率、音调微调率及带宽。编码策略将任务与设备分配映射为和声,目标是最小化完成时间,同时确保满足各种约束条件。
|
29天前
|
算法
【算法】递归、搜索与回溯——简介
【算法】递归、搜索与回溯——简介
|
2月前
|
大数据 UED 开发者
实战演练:利用Python的Trie树优化搜索算法,性能飙升不是梦!
【7月更文挑战第19天】Trie树,又称前缀树,是优化字符串搜索的高效数据结构。通过利用公共前缀,Trie树能快速插入、删除和查找字符串。
67 2
|
2月前
|
机器学习/深度学习 数据采集 算法
Python实现SSA智能麻雀搜索算法优化支持向量机回归模型(SVR算法)项目实战
Python实现SSA智能麻雀搜索算法优化支持向量机回归模型(SVR算法)项目实战
123 1
|
6天前
|
算法 BI Serverless
基于鱼群算法的散热片形状优化matlab仿真
本研究利用浴盆曲线模拟空隙外形,并通过鱼群算法(FSA)优化浴盆曲线参数,以获得最佳孔隙度值及对应的R值。FSA通过模拟鱼群的聚群、避障和觅食行为,实现高效全局搜索。具体步骤包括初始化鱼群、计算适应度值、更新位置及判断终止条件。最终确定散热片的最佳形状参数。仿真结果显示该方法能显著提高优化效率。相关代码使用MATLAB 2022a实现。
|
6天前
|
算法 数据可视化
基于SSA奇异谱分析算法的时间序列趋势线提取matlab仿真
奇异谱分析(SSA)是一种基于奇异值分解(SVD)和轨迹矩阵的非线性、非参数时间序列分析方法,适用于提取趋势、周期性和噪声成分。本项目使用MATLAB 2022a版本实现从强干扰序列中提取趋势线,并通过可视化展示了原时间序列与提取的趋势分量。代码实现了滑动窗口下的奇异值分解和分组重构,适用于非线性和非平稳时间序列分析。此方法在气候变化、金融市场和生物医学信号处理等领域有广泛应用。
|
29天前
|
算法
基于模糊控制算法的倒立摆控制系统matlab仿真
本项目构建了一个基于模糊控制算法的倒立摆控制系统,利用MATLAB 2022a实现了从不稳定到稳定状态的转变,并输出了相应的动画和收敛过程。模糊控制器通过对小车位置与摆的角度误差及其变化量进行模糊化处理,依据预设的模糊规则库进行模糊推理并最终去模糊化为精确的控制量,成功地使倒立摆维持在直立位置。该方法无需精确数学模型,适用于处理系统的非线性和不确定性。
基于模糊控制算法的倒立摆控制系统matlab仿真