最短路径—SPFA算法(邻接矩阵+链式前向星+邻接表实现)

最短路径—SPFA算法(邻接矩阵+链式前向星+邻接表实现)

注:阅读此篇默认你已经知晓了松弛等相关概念,博主建议你最好可以先学会最简单的Bellman-ford算法再来学这篇,这样效果会好很多。Bellman-ford博文指路:https://blog.csdn.net/hzf0701/article/details/107686720

一、简介

SPFA算法是Bellman-ford算法队列优化的别称,通常用于求含负权边的单源最短路径,以及判负权环。SPFA 最坏情况下复杂度和朴素 Bellman-Ford 相同,为 O(VE)。思想和Bellman算法一样,相当于Bellman-ford算法的进阶版。

适用条件:

  1. 单源最短路径(从源点s到其它所有顶点v);
  2. 有向图&无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图);
  3. 边权可正可负(如有负权回路输出错误提示);

二、原理

SPFA则为针对Bellman-ford的缺点做的使每次做的松弛操作更有效的优化。发现在Bellman-ford第一次大松弛中只有松弛从源点出来的边才是有效的,松弛时当边的起点有效(被松弛过)时松弛操作才有效。那我们就不妨每次都只做有效的松弛操作,建一个队列,开始时将源点入队,每次从队头弹出head,尝试松弛所有head的邻接点,若松弛成功且该邻接点不在队中,就把它入队;否则什么也不干。时间复杂度为O(vm),v是一个平均值为2的常数。(因为当我们每次都做有效的松弛操作时,就会发现跑最短路快了很多,在大多数情况下可以吊打各个最短路算法。当然也特别容易被特殊数据卡,此时时间复杂度容易退化为O(nm))。类似的,spfa每做“一层”(层的概念类似于BFS的层)松弛操作就会确定所有最短路的一条边,故最极限的情况spfa做n层松弛操作就可以求出一个单源最短路了。


三、步骤演示

举个例子:

img

img


四、具体实现(含有判断负环代码)

注:SPFA算法保证每个顶点至多入队列n次(n为顶点总数)更新出到所有点的最短距离,如果某个点入队列次数大于n次表示图中有负环,所以我们这里开了一个 数组cnt[maxn]来记录每个顶点的入队次数。

1、邻接矩阵实现

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<string>
#include<stack>
#include<queue>
#include<cstring>
#include<map>
#include<iterator>
#include<list>
#include<set>
#include<functional>
#include<memory.h>

using namespace std;

const int maxn=105;
const int inf=0x3f3f3f3f;
int n,m;//顶点数与边数。
int graph[maxn][maxn];//针对稠密图。
int dis[maxn];//存储所有顶点到源点的距离。
int cnt[maxn];//cnt[i]记录的是i顶点的入队次数。若大于n,则必然存在负环。
bool visited[maxn];//判断是否在队列中。
void init(){
	memset(graph,inf,sizeof(graph));
	memset(dis,inf,sizeof(dis));
	memset(visited,false,sizeof(visited));
	memset(cnt,0,sizeof(cnt));
}
int spfa(int S){
	queue<int> q;//顶点队列
	dis[S]=0;
	q.push(S);//源点入队。
	cnt[S]++;
	int temp;
	visited[S]=true;
	int flag=0;//判断是否存在负环
	while(!q.empty()){
		temp=q.front();
		q.pop();
		visited[temp]=false;//已经出队,改标志位。
        //松弛操作
		for(int i=1;i<=n;i++){
			if(dis[i]>dis[temp]+graph[temp][i]){
				dis[i]=dis[temp]+graph[temp][i];//更新最短路径
				if(!visited[i]){
					q.push(i);
					cnt[i]++;
					if(cnt[i]>n){flag=1;return flag;}
					visited[i]=true;
				}
			}
		}
	}
	return flag;
}
int main(){
	while(cin>>n>>m){
		int u,v,w;
		init();
		for(int i=0;i<m;i++){
			cin>>u>>v>>w;
			graph[u][v]=w;
			graph[v][u]=w;
		}
		int S,E;//起点与终点。
		cin>>S>>E;
		int result=spfa(S);
		if(result)cout<<"存在负环"<<endl;
		else cout<<dis[E]<<endl;
	}
	return 0;
}

2、链式前向星实现

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<string>
#include<stack>
#include<queue>
#include<cstring>
#include<map>
#include<iterator>
#include<list>
#include<set>
#include<functional>
#include<memory.h>

using namespace std;

const int maxn=105;//顶点最大数
const int M=1e4;//边最大数
const int inf=0x3f3f3f3f;
int n,m;//顶点数与边数。
int tot;//递增序号,用来存储。
int cnt[maxn];//cnt[i]记录的是i顶点的入队次数。若大于n,则必然存在负环。
typedef struct Edge{
	int next;//当前边的下一条边。
	int to;//边的终端节点
	int w;//边权值。
}Edge;
Edge edge[M];//用边来表示图。
int head[maxn];//head[i]表示以i为起点的第一条边的编号,若为-1则表示以该起点没有边。
int dis[maxn];//存储所有顶点到源点的距离。
bool visited[maxn];//判断是否在队列中。
void init(){
	memset(dis,inf,sizeof(dis));
	memset(visited,false,sizeof(visited));
	memset(head,-1,sizeof(head));
	memset(cnt,0,sizeof(cnt));
	tot=0;
}
void add(int u,int v,int w){
	edge[tot].to=v;
	edge[tot].w=w;
	edge[tot].next=head[u];//插在第一条边前面。
	head[u]=tot++;//改变第一条边的编号。
}
int spfa(int S){
	queue<int> q;
	dis[S]=0;
	int temp;
	q.push(S);
	cnt[S]++;
	visited[S]=true;
	int flag=0;//标志位,判断是否存在负环。
	while(!q.empty()){
		temp=q.front();
		q.pop();
		visited[temp]=false;
		int v,w;
		for(int i=head[temp];i!=-1;i=edge[i].next){
			v=edge[i].to;//保存边的终端节点。
			w=edge[i].w;//保存边权值。
			if(dis[v]>dis[temp]+w){
                //松弛操作。
				dis[v]=dis[temp]+w;
				if(!visited[v]){
					q.push(v);
					cnt[v]++;
					if(cnt[v]>n){flag=1;return flag;}
					visited[v]=true;
				}
			}
		}
	}
	return flag;
}
int main(){
	while(cin>>n>>m){
		int u,v,w;
		init();
		for(int i=0;i<m;i++){
			cin>>u>>v>>w;
		    add(u,v,w);//这里针对的是无向图,有向图加边只加一次。
			add(v,u,w);
		}
		int S,E;//起点与终点。
		cin>>S>>E;
		int result=spfa(S);
		if(result)cout<<"存在负环"<<endl;
		else cout<<dis[E]<<endl;
	}
	return 0;
}

3、vector邻接表实现

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<string>
#include<stack>
#include<queue>
#include<cstring>
#include<map>
#include<iterator>
#include<list>
#include<set>
#include<functional>
#include<memory.h>

using namespace std;

const int maxn=105;//顶点最大数
const int M=1e4;//边最大数
const int inf=0x3f3f3f3f;
int n,m;//顶点数与边数。
int cnt[maxn];//cnt[i]记录的是i顶点的入队次数。若大于n,则必然存在负环。
typedef struct Edge{
	int to;//边的终端节点
	int w;//边权值。
	Edge(int to,int w):to(to),w(w){}//构造函数
}Edge;
vector<Edge> graph[maxn];//用vector模拟邻接表
int dis[maxn];//存储所有顶点到源点的距离。
bool visited[maxn];//判断是否在队列中。
void init(){
	memset(cnt,0,sizeof(cnt));
	memset(dis,inf,sizeof(dis));
	memset(visited,false,sizeof(visited));
}
int spfa(int S){
	queue<int> q;
	dis[S]=0;
	int temp;
	q.push(S);
	cnt[S]++;
	visited[S]=true; //入队即true。
	int flag=0;         //标志,若为真,则表示存在负环。
	while(!q.empty()){
		temp=q.front();
		q.pop();
		visited[temp]=false; //出队则false。
		int v,w;
		int t=graph[temp].size();//避免多次调用此函数。
        //松弛操作
        for(int i=0;i<t;i++){
			v=graph[temp][i].to;
			w=graph[temp][i].w;
			if(dis[v]>dis[temp]+w){
				dis[v]=dis[temp]+w;//更新最短路径
				if(!visited[v]){
					//判断是否在队列中
					q.push(v);
					cnt[v]++;
					if(cnt[v]>n){flag=1;return flag;}
					visited[v]=true;
				}
			}
        }
	}
	return flag;
}
int main(){
	while(cin>>n>>m){
		int u,v,w;
		init();
		for(int i=0;i<m;i++){
			cin>>u>>v>>w;
		    graph[u].push_back(Edge(v,w));//这里针对的是无向图,有向图加边只加一次。
		    graph[v].push_back(Edge(u,w));
		}
		int S,E;//起点与终点。
		cin>>S>>E;
		int result=spfa(S);
		if(result)cout<<"存在负环"<<endl;
		else cout<<dis[E]<<endl;
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/hzf0701/article/details/107691013
今日推荐