【图论】Spfa算法求最短路(长得像Dijkstra的,Bellman_Ford的优化算法)

图论与数据结构 同时被 2 个专栏收录
24 篇文章 1 订阅

一、前言

在之前的学习中,我们学过了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,喜欢我的博客就点个赞吧,更多图论与数据结构知识点请见作者专栏《图论与数据结构》

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值