一、前言
在之前的学习中,我们学过了Dijkstra算法和Bellman_Ford算法求最短路问题(Dijkstra求最短路)(Bellman_Ford求有限制的最短路),而今天我们要继续图论的学习。名义上,Spfa算法是Bellman_Ford的优化算法,但是…它的代码长得和Dijkstra算法真的好像啊,真的好像啊。
二、概念介绍
算法思路: 在Bellman_Ford算法中,我们需要依次遍历每一条边(a----->b,长为w)来更新我们的dis[b]。但是我们发现,如果dis[a]在循环中如果一直没有出现变化,那么公式(dis[b]=min(dis[b],te[a]+w))就一直不会更新我们的dis[b],做了很多的无用操作,对程序的运行速度造成了比较大的影响。那我们需要这样去想:我们可不可以用一个方法记录一下a的状态,只有当dis[a]发生了变化时,我们才去更新所有以a为起点的点呢?这,就是Spfa算法的思路。而在时间复杂度上,虽然spfa算法的时间复杂度有退化的可能性,但基本上优于Bellman_Ford。
实现思路: 在代码中,我们会使用一个队列q去存下所有dis发生了改变的值(首先把起点放进队列),然后依次遍历与它相连的点,如果字节点通过比较后发生了更新而且队列中没有这个子节点,那我们就把子节点也放进队列q。在遍历完一个点的所有子节点后,我们把这个点移出队列。然后只要队列不空,我们就一直循环。如果这里没有看懂,在之后的代码中我会解释。
负权边的处理问题: 在这里我们要讲一讲图中负权边对算法的影响,由于spfa仍然求的是没有次数限制的最短路问题,所以如果出现一个负权边形成的环,那么队列是会陷入一个死循环从而出错。为了避免这种情况,我们可以用一个数组记录下每一个点进入队列q的次数,如果进入的次数达到了一个比较大的明显不正常的值,我们就认为存在负权环,从而退出算法函数。
三、代码实现基础Spfa
例题链接:Acwing 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
在题目中明确提出了是没有负权回路的,所以我们可以放心的使用spfa算法了
看懂下面的代码你需要提前知道以下知识点:
1.链式前向星存图(链式前向星)
2.作者重写的memset函数(“#define mem(a,b) memset(a,b,sizeof(a))”)
3.BFS宽度优先搜索的基本思想
4.C++STL中queue队列数据结构的基本用法
//#pragma GCC optimize(2)
#include<iostream>
#include<iomanip>
#include<cstdio>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#include<vector>
#include<map>
#include<stack>
#include<set>
#include<bitset>
#include<ctime>
#include<cstring>
#include<list>
#define ll long long
#define ull unsigned long long
#define INF 0x3f3f3f3f
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
typedef pair<int, int> PII;
const int N = 1e5 + 7;
int e[N], ne[N], w[N], h[N], id = 1; //链式前向星
int n, m;
int dis[N]; //dis[i]代表从起点到i的距离
bool ch[N]; //用来判断一个点是否在队列中
void add(int a, int b, int c) //加边函数
{
e[id] = b;
w[id] = c;
ne[id] = h[a];
h[a] = id;
id++;
}
int spfa()
{
mem(dis, 0x3f); //距离的初始化
dis[1] = 0; //起点到自己的距离为0
queue<int>q;
q.push(1); //先把起点放进去
ch[1] = true; //记录一下起点放进去了
while (!q.empty()) //循环条件:队列非空
{
int f = q.front(); //取出队首元素
q.pop(); //把队首踢出去
ch[f] = false; //记录一下队首已经被踢出去了
for (int i = h[f]; i != -1; i = ne[i]) //链式前向星遍历所有以f为起点的边
{
int j = e[i];
if (dis[j] > dis[f] + w[i]) //如果点j发生了更新
{
dis[j] = dis[f] + w[i]; //更新一下
if (!ch[j]) //如果点j没在队列中
{
ch[j] = true; //把点j放进队列
q.push(j);
}
}
}
}
return dis[n]; //输出最后的结果
}
void solve()
{
mem(h, -1); //初始化链式前向星
cin >> n >> m;
for (int i = 0; i < m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
int t = spfa();
if (t == INF) //如果是INF代表到不了
cout << "impossible" << endl;
else
cout << t << endl;
}
int main()
{
//std::ios::sync_with_stdio(false);
//cin.tie(0), cout.tie(0);
solve();
return 0;
}
四、代码实现Spfa对负权环的判断
例题链接:Acwing 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
分析:这道题中并没有要求我们求出最短路的距离,所以dis数组可以不初始化为0x3f3f3f3f,初始化为0就可以达到我们想要的效果了。因为0大于负数,而遇到负权边的时候dis会进行更新。除此之外,这道题并没有说明起点,所以我们需要在最开始把所有的点push进队列q。
//#pragma GCC optimize(2)
#include<iostream>
#include<iomanip>
#include<cstdio>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#include<vector>
#include<map>
#include<stack>
#include<set>
#include<bitset>
#include<ctime>
#include<cstring>
#include<list>
#define ll long long
#define ull unsigned long long
#define INF 0x3f3f3f3f
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 7;
int e[N], ne[N], w[N], h[N], id = 1; //链式前向星
int n, m;
int dis[N]; //dis[i]代表从起点到i的距离
bool ch[N]; //用来判断一个点是否在队列中
int num[N]; //num[i]代表走到i经过了多少条边
void add(int a, int b, int c) //加边函数
{
e[id] = b;
w[id] = c;
ne[id] = h[a];
h[a] = id;
id++;
}
int spfa()
{
/*本题不是求距离,可以不用初始化dis*/
queue<int>q;
for (int i = 1; i <= n; i++)
{
q.push(i);
ch[i] = true;
}
while (!q.empty()) //循环条件:队列非空
{
int f = q.front(); //取出队首元素
q.pop(); //把队首踢出去
ch[f] = false; //记录一下队首已经被踢出去了
for (int i = h[f]; i != -1; i = ne[i]) //链式前向星遍历所有以f为起点的边
{
int j = e[i];
if (dis[j] > dis[f] + w[i]) //如果点j发生了更新
{
dis[j] = dis[f] + w[i]; //更新一下
num[j] = num[f] + 1;
if (num[j] >= n) //经过的边的数量如果大于了n-1,那就说明至少重复走了一个点,有负权环(只有存在负权环,才可能在求最短路的时候两次经过一个点)
return -1;
if (!ch[j]) //如果点j没在队列中
{
ch[j] = true; //把点j放进队列
q.push(j);
}
}
}
}
return 1; //输出最后的结果
}
void solve()
{
mem(h, -1); //初始化链式前向星
cin >> n >> m;
for (int i = 0; i < m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
int t = spfa();
if (t == -1)
cout << "Yes" << endl;
else
cout << "No" << endl;
}
int main()
{
//std::ios::sync_with_stdio(false);
//cin.tie(0), cout.tie(0);
solve();
return 0;
}
作者:Avalon Demerzel,喜欢我的博客就点个赞吧,更多图论与数据结构知识点请见作者专栏《图论与数据结构》