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

来源:互联网 发布:淘宝视频教程全集 编辑:程序博客网 时间:2024/06/07 01:17



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算法还可以检测一个图是否含有负权回路,如果在进行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(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;}


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

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

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