单源最短路径--Bellman-Ford算法及SPFA

Bellman-Ford——解决负权边

dijkstra算法虽然好,但是它不能解决带有负权边(边的权值为负数)的图,Bellman-Ford算法的核心代码只有4行,可以完美地解决带有负权边的图

for(k=1;k<=n-1;k++){
	for(i=1;i<=m;i++){
		if(dis[v[i]]>dis[u[i]]+w[i]){//能否通过u[i]→v[i]这条边,使得1号顶点到v[i]号顶点的距离变短。即dijkstra算法中的松弛
			dis[v[i]]=dis[u[i]]+w[i];
		}
	}
}

n为顶点的个数,m为边的个数,dis[ ]数组和dijkstra算法一样,记录源点到其余各个顶点的最短路径,u[ ] v[ ] w[ ] 三个数组记录边的信息,例如第i条边存储在u[i]、v[i]和w[i] 中,表示从顶点u[i]到顶点v[i]这条边 权值为w[i].

第一次在对所有边进行松弛后,得到的是从1号顶点“只能经过一条边”到达其余各顶点的最短路径长度,第二轮对所有的边进行松弛之后,得到的是1号顶点“最多经过两条边”,到达其余各顶点的最短路径长度….而只需要进行n-1轮就可以了,因为在一个含有n个顶点没有负权回路的图中,任意两点之间的最短路径最多包含n-1条边,最短路径肯定是一个不包含回路的简单路径。  

《单源最短路径--Bellman-Ford算法及SPFA》

此外,Bellman-Ford算法还可以检测一个图是否含有负权回路,如果在进行n-1轮松弛之后,仍然存在变化,那么该图必然存在负权回路。

if(dis[v[i]]>dis[u[i]]+w[i])  dis[v[i]]=dis[u[i]]+w[i]; 也就是说在n-1轮松弛之后仍然可以继续成功松弛,那么此图必然存在负权回路。

它的时间复杂度为O(N*M),比dijkstra算法时间复杂度还要高,可以进行简单的优化,可以添加一个一维数组来备份数组dis,如果在新的一轮松弛中数组dis没有发生变化,则可以提前跳出循环。

完整代码如下:

#include<cstdio>
const int inf=99999999;
int dis[111],u[111],v[111],w[111],bak[111];
int n,m;
void init(){
	for(int i=1;i<=n;i++){
		dis[i]=inf;
	} 
	dis[1]=0;
}
int main(){
	int i,k;
	scanf("%d%d",&n,&m);//读入n个顶点,m条边
	//读入边
	for(i=1;i<=m;i++){
		scanf("%d%d%d",&u[i],&v[i],&w[i]);
	}
	//初始化 
	init();	
	for(k=1;k<=n-1;k++){
		for(i=1;i<=n;i++){
			bak[i]=dis[i];
		}
		for(i=1;i<=m;i++){
			if(dis[v[i]]>dis[u[i]]+w[i]){
				dis[v[i]]=dis[u[i]]+w[i];
			}
		}
		//松弛完毕后检测dis数组是否有更新
		int check=0;
		for(i=1;i<=n;i++){
			if(bak[i]!=dis[i]){
				check=1;
				break;
			} 
		} 
		if(check==0)
			break;//如果dis数组没有更新,提前退出循环结束算法
	}
	//检测负权回路
	int flag=0;
	for(i=1;i<=m;i++){
		if(dis[v[i]]>dis[u[i]]+w[i]){
			flag=1;
		}
	} 
	if(flag=1){
		printf("此图含有负权回路"); 
	}
	else
	{
		for(i=1;i<=n;i++){
		printf("%d ",dis[i]);
    	}  
    }
	return 0;
} 

《单源最短路径--Bellman-Ford算法及SPFA》

Bellman-Ford算法的队列优化

SPFA(Shortest Path Faster Algorithm)是Bellman-Ford算法的一种队列优化,每次仅对最短路程发生变化了的相邻边执行松弛操作,用队列来维护这些最短路程发生了变化的点

例如:

5 7

1 2 2

1 5 10

2 3 3

2 5 7

3 4 4

4 5 5

5 3 6

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
int n,m;
int u[111],v[111],w[111];//大小要比m的最大值大1 
int inf=99999999; 
int main(){
	int i,j,k;
	//first要比n的最大值大1,next要比m的最大值大1 
	int first[111],next[111]; 
	int dis[111],vis[111];
	int que[222]={0},head=1,tail=1;//定义并初始化队列 
	cin>>n>>m;
	//初始化dis数组,这里是1号顶点到其余各个顶点的初始路程 
	for(i=1;i<=n;i++){
		dis[i]=inf;
	}
	dis[1]=0;
	memset(vis,0,sizeof(vis));
	//初始化first数组 为-1,表示1~n顶点暂时都没有边
	for(i=1;i<=n;i++){
		first[i]=-1;
	} 
	for(i=1;i<=m;i++){
		cin>>u[i]>>v[i]>>w[i];
		//建立邻接表的关键
		next[i]=first[u[i]];
		first[u[i]]=i; 
	}
	//1号顶点入队
	que[tail]=1;tail++;
	vis[1]=1;//标记1号顶点已经入队
	while(head<tail)//队列不为空的时候循环
	{
		k=first[que[head]];//队首顶点
		while(k!=-1){
			if(dis[v[k]]>dis[u[k]]+w[k])
			{
				//更新顶点1到顶点v[k]的路程 
				dis[v[k]]=dis[u[k]]+w[k];
				//vis数组用来判断顶点v[k]是否在队列中,如果不使用数组来标记的话, 
				//判断一个顶点是否在队列中每次都需要从队列的head到tail扫一遍,浪费时间 
				if(vis[v[k]]==0){
					//入队
					que[tail]=v[k];
					tail++;
					vis[v[k]]=1;//同时标记顶点v[k] 
				} 
			}
			k=next[k]; 
		}
		//出队
		vis[que[head]]=0;
		head++; 
	 } 
	 //输出1号顶点到其余各个顶点的最短路径
	 for(i=1;i<=n;i++){
	 	cout<<dis[i]<<" ";
	 } 
	 cout<<endl;
	 return 0;
}

《单源最短路径--Bellman-Ford算法及SPFA》

 

关键:只有那些在前一遍松弛中改变了最短路程估计值的顶点,才有可能引起它们邻接点最短路程估计值发生改变,因此用一个队列来存放被成功松弛的顶点,之后只对队列中的点进行处理,降低了算法的时间复杂度。

初始将源点加入队列,每次从队首(head)取出一个顶点,并对其相邻的所有顶点进行松弛尝试,且这个相邻的顶点不在队列中,则把它加到队列中,对当前顶点处理完毕后立即出队,并对下一个新队首进行如上操作,直到队列为空时算法结束,这里用了vis数组记录每个顶点是否处在队列中。  与bfs类似,不同的是bfs在一个顶点出队后通常不会重新进入队列,而这里一个顶点很可能在出队列后再次被放入队列,也就是当一个顶点的最短路程估计值变小后,需要对其所有出边进行松弛,如果这个估计值再次变小时,仍需要对其所有出边再次进行松弛。

如果 某个点进入队列的次数超过n次,那么这个图肯定存在负环。

 

《单源最短路径--Bellman-Ford算法及SPFA》

 

    原文作者:Bellman - ford算法
    原文地址: https://blog.csdn.net/m0_37345402/article/details/78070065
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞