求单源最短路径、长度及判断负环——SPFA模板

SPFA已经死了… 当然,只是在出题人针对的情况下,容易退化为Bellman-ford的复杂度,在处理负权图方面,dijkstra做不到,所以它仍有着dijkstra所不能及的优势(当然负权图也还能卡SPFA,使其达到指数复杂度 )。

情景:
给出一个有向图,请输出从某一点s出发到所有点的最短路径长度,若不可达,输出 2 31 − 1 2^{31}-1 2311(即URA)。为了方便,代码中将结点编号 1~n 改为 0~n-1,最后再以 1~n 输出。‘

采用邻接表G形式存储图。

与dijkstra的堆优化相比,可以发现,dijkstra中堆是对进行松弛操作后的源点到各点的距离进行贪心,即构建<距离,结点>的结构体,而SPFA是将进行了松弛操作的后继加入队列,然后以队首后继作为新一轮搜索的前趋,搜索该点各个边,这样看采用队列的SPFA是一种bfs思想。

若需要输出最短路径,参考dijkstra模板,同样只需要设置前趋数组pre,在进行松弛操作后存储点的前趋,最后从终点往前回溯即可。

若需要练习可移步例题:洛谷-【模板】单源最短路径(弱化版)

队列的SPFA
同一时间内队列中不能存在同个结点,所以需要vis数组辅助

#include<iostream>
#include<queue>
#include<vector>
#include<stack>
using namespace std;
const int URA=(2<<30)-1;
const int MAX_N=1e4+10;
const int INF=1e9+10;
struct edge
{
	int to,cost;	
	edge(int t,int c):to(t),cost(c){}
};
int n,m,s;
int dis[MAX_N];
vector<edge> G[MAX_N];
bool vis[MAX_N];
void SPFA(int s)
{
	queue<int> q;
	for(int i=0;i<n;i++)
		dis[i]=INF;
	dis[s]=0;
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		vis[u]=false;//出队,但可以重新进入
		for(int i=0;i<G[u].size();i++)
		{
			int v=G[u][i].to;
			if(dis[u]+G[u][i].cost<dis[v])
			{
				dis[v]=dis[u]+G[u][i].cost;
				if(!vis[v])//不在队列中
				{
					vis[v]=true;
					q.push(v);		
				}		
			}
		}
	}
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>s;
	int u,v,w;
	for(int i=0;i<m;i++)
	{
		cin>>u>>v>>w;
		G[u-1].push_back(edge(v-1,w));
	}
	SPFA(s-1);
	for(int i=0;i<n;i++)
		cout<<(dis[i]==INF?URA:dis[i])<<" ";
	return 0;
}

堆的SPFA:
堆中允许存在多个同名结点,因此不需要vis数组。在一些非负权图中表现不错,但是在带负权边的图中容易被卡导致复杂度暴增。

#include<iostream>
#include<queue>
#include<vector>
#include<stack>
using namespace std;
const int URA=(2<<30)-1;
const int MAX_N=1e4+10;
const int INF=1e9+10;
struct edge
{
	int to,cost;	
	edge(int t,int c):to(t),cost(c){}
};
int n,m,s;
int dis[MAX_N];
vector<edge> G[MAX_N];
void SPFA(int s)
{
	priority_queue<int,vector<int>,greater<int> >q;
	for(int i=0;i<n;i++)
		dis[i]=INF;
	dis[s]=0;
	q.push(s);
	while(!q.empty())
	{
		int u=q.top();
		q.pop();
		for(int i=0;i<G[u].size();i++)
		{
			int v=G[u][i].to;
			if(dis[u]+G[u][i].cost<dis[v])
			{
				dis[v]=dis[u]+G[u][i].cost;
				q.push(v);
			}
		}
	}
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>s;
	int u,v,w;
	for(int i=0;i<m;i++)
	{
		cin>>u>>v>>w;
		G[u-1].push_back(edge(v-1,w));
	}
	SPFA(s-1);
	for(int i=0;i<n;i++)
		cout<<(dis[i]==INF?URA:dis[i])<<" ";
	return 0;
}

SPFA判断带负环的图

例题:洛谷-【模板】负环

有两种思路:
1.队列SPFA中,若点入队次数>=n次,则存在负环。
这个结论怎么来的呢,思考了很久 ,考虑某个点,不计初始时源点入队那次,我们需要观察结点的入队条件:需要进行松弛操作且不在当前队列中,也就是说,结点在入队前成为了源点到某点的最短路径中的一点,而根据最短路径的性质,同名结点只能出现一次,所以该点最多在其他n-1个点的最多路径中都出现一次,即最多为n-1次。若>=n,说明该点在某条最短路径至少出现两次,该路径必存在环,进行了多余的松弛,也就说明存在负环。

2.根据最短路径的性质,有n个顶点的图的一条最短路径最多含有n-1条边,最多经过n个点,所以只需要判断某条最短路径是否超过了n个点或者超过n-1条边,是则判断含负环。具体操作为初始化源点cnt为1,对于每次松弛操作dis+w(u,v),说明源点到点v的最短路径经过点u,需要增加一条边,使cnt[v]=cnt[u]+1,随后判断cnt[v]这条路径上的边数是否>=n。

方法1在入队次数多,权值大时有爆int风险,而且一般方法2更快,这里给出方法2模板,采用判断最短路径所经过的边数是否>=n:

#include<iostream>
#include<queue>
#include<vector>
#include<stack>
#include<string.h>
using namespace std;
const int URA=(2<<30)-1;
const int MAX_N=2e3+10;
const int INF=1e9+10;
struct edge
{
	int to,cost;	
	edge(int t,int c):to(t),cost(c){}
};
int t,n,m,s;
int dis[MAX_N];
vector<edge> G[MAX_N];
bool vis[MAX_N];
int cnt[MAX_N];//入队次数
bool SPFA(int s)//true说明有负环
{
	queue<int> q;
	for(int i=0;i<n;i++)
		dis[i]=INF;
	dis[s]=0;
	q.push(s);
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		vis[u]=false;//出队,但可以重新进入
		for(int i=0;i<G[u].size();i++)
		{
			int v=G[u][i].to;
			if(dis[u]+G[u][i].cost<dis[v])
			{
				cnt[v]=cnt[u]+1;
				if(cnt[v]>=n)//有负环
					return true;
				dis[v]=dis[u]+G[u][i].cost;
				if(!vis[v])//不在队列中
				{
					vis[v]=true;
					q.push(v);		
				}		
			}
		}
	}
	return false;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>t;
	while(t--)
	{
		cin>>n>>m;
		int u,v,w;
		while(m--)
		{
			cin>>u>>v>>w;
			if(w>=0)
			{
				G[u-1].push_back(edge(v-1,w));
				G[v-1].push_back(edge(u-1,w));
			}
			else
				G[u-1].push_back(edge(v-1,w));
		}
		if(SPFA(0))
			cout<<"YES"<<"\n";
		else
			cout<<"NO"<<"\n";
		memset(vis,0,sizeof(vis));
		memset(cnt,0,sizeof(cnt));
		memset(dis,0,sizeof(dis));
		for(int i=0;i<n;i++)
			G[i].clear();
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值