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次,那么这个图肯定存在负环。