Bellman-Ford算法原始版本中,在每次实施一次松弛操作后,就会有一些顶点已经求得其最短路,此后这些顶点的最短路的估计值就会一直保持不变,不再受后续松弛操作的影响,但是每次还要判断是否需要松弛,这里浪费了时间。这就启发我们:每次仅对最短路估计值发生变化的顶点的所有出边执行松弛操作。
算法大致如下:
每次选取队首顶点u,对顶点u的所有出边进行松弛操作。例如有一条u-->v的边,如果通过u-->v这条边使得源点到顶点v的最短路程变短(dis[u]+e[u][v]<dis[v]),且顶点v不在当前的队列中,就将顶点v放入队尾。需要注意的是,同一个顶点同时在队列中初选多次是毫无意义的,所以我们需要一个数组来判重。在对顶点u的所有出边松弛完毕后,就将顶点u出队。接下来不断从队列中取出新的队首顶点再进行如上操作,直至队列空为止
演示:
input: 5 7 //代表5个顶点,7条边
x y z //顶点x到顶点y的边权值为z
1 2 2
1 5 10
2 3 3
2 5 7
3 4 4
4 5 5
5 3 6
我们用数组dis来存放1号顶点到其余各个顶点的最短路径。起初dis[1]=0其余为无穷大。接下来将1号顶点入队。队列这里用一个数组que以及两个 分别指向队列头和尾的变量head和tail来实现
1 2 3 4 5 0 1 2 3 4 5 6 7 8 9
dis[] 0 INF INF INF INF que 1 |
| \
head tail
先来看当前队首1号顶点的边1-->2,看通过1-->2能否让1号顶点到2号顶点 的路径(即dis[2])变短,也就是说先来比较dis[2]和dis[1]+(1-->2)的大小dis[2]原来为INF,dis[1]+(1-->2)的值为2.因此松弛成功,dis[2]的值从INF更新为2,并且当前2号顶点不在队列中,因此将2号顶点入队.
1 2 3 4 5 0 1 2 3 4 5 6 7 8 9
dis[] 0 2 INF INF INF que 1 2 |
| \
head tail
同样,对1号顶点剩余的出边进行如上操作,处理完毕后数组dis和队列que如下
1 2 3 4 5 0 1 2 3 4 5 6 7 8 9
dis[] 0 2 INF INF 10 que 1 2 5 |
| \
head tail
对1号顶点处理完毕后,就将1号顶点出队(head++即可),在对新队首2号顶点进行 如上操作。在处理2-->5这条边时需要特别注意下,2-->5这条边虽然可以让1号顶点到5号顶点的路程变短(dis[5]-->> 10-->9),但是5号顶点已经在队列中了,因此5号顶点不能再次入队。对2号处理完毕后数组dis和队列que状态如下:
1 2 3 4 5 0 1 2 3 4 5 6 7 8 9
dis[] 0 2 5 INF 9 que 1 2 5 3 |
| \
head tail
在对2号顶点处理完毕后,需要将2号顶点出队,并依次对剩下的顶点做相同的处理,直到队列为空。最终数组dis和队列que状态如下:
1 2 3 4 5 0 1 2 3 4 5 6 7 8 9
dis[] 0 2 5 9 10 que 1 2 5 3 4 |
|\
head tail
#include<stdio.h>
int main(){
int n,m,i,j,k;
//n表示顶点个数,m表示边的条数
//i,j,k表示计数作用
int u[8],v[8],w[8];
//u,v和w的数组大小要根据实际情况来设置,要比m的最大值要大1
// u[] --> v[] 权值为w[]
int first[6],next[8];
//first要比n的最大值要大1,next要比m的最大值要大1
int dis[6]={0},book[6]={0};
//dis数组记录1号顶点到其余各顶点的路程
//book数组用来记录哪些顶点已经在队列中
int que[101]={0},head=1,tail=1;//定义一个队列,并初始化队列
int INF = 99999999;
scanf("%d%d",&n,&m);
//初始化
for(i=1;i<=n;i++)//初始化dis数组,这里是1号顶点到其余各个顶点的初始路程
dis[i]=INF;
dis[1]=0;
for(i=1;i<=n;i++)
book[i] = 0; //初始化为0,刚开始都不在队列中
for(i=1;i<=n;i++)//初始化first数组下表1~n的值为-1.表示1~n顶点暂时都没有边
first[i]=-1;
for(i=1;i<=m;i++){
//读入每一条边 u[] --> v[] 权值为w[]
scanf("%d%d%d",&u[i],&v[i],&w[i]);
//下面两句是建立邻接表的关键
next[i] = first[u[i]];
first[u[i]]=i;
}
/*
* 模拟:
* 例如input n-5,m=7
* u v w
* i=1 1 2 2
* i=2 1 5 10
* i=3 2 3 3
* i=4 2 5 7
* i=5 3 4 4
* i=6 4 5 5
* i=7 5 3 6
*
* next[1] = first[1]=-1;
* first[1] = 1;
*
* next[2] = first[1]=1;
* first[1] = 2;
*
* next[3] = first[2]=-1;
* first[2] =3;
*
* next[4] = first[2]=3;
* first[2] = 4;
*
* next[5] = first[3]=-1;
* first[3] = 5;
*
* next[6] = first[4]=-1;
* first[4] = 6;
*
* next[7] = first[5]=-1;
* first[5] = 7;
*/
//1号顶点入队
que[tail]=1; tail++;
book[1]=1;//标记1号顶点已经入队
while(head<tail){//队列不为空的时候循环
k=first[que[head]]; //当前需要处理的队首顶点
/*
first[1] = 2;
first[2] = 4;
first[3] = 5;
first[4] = 6;
first[5] = 7;
*/
while(k!=-1){ //扫描当前顶点所有的边
if(dis[v[k]]>dis[u[k]]+w[k]){//判断是否松弛成功
dis[v[k]] = dis[u[k]] + w[k];//更新顶点1到顶点v[k]的路程
//这的book数组用来判断顶点v[k]是否在队列中
/*如果不使用一个数组来标记的话,判断一个顶点是否在队列中每次
都需要从队列的head到tail扫一遍,很浪费时间*/
if(book[v[k]]==0){//0表示不在队列中,将顶点v[k]加入队列中
//下面两句为入队操作
que[tail] = v[k];
tail++;
//同时标记顶点v[k]已经入队
book[v[k]] =1;
}
}
k = next[k];
/*
next[1] = -1;
next[2] = 1;
next[3] = -1;
next[4] = 3;
next[5] = -1;
next[6] = -1;
next[7] = -1;
*/
}
//出队
book[que[head]] = 0;
head++;
}
//输入1号顶点到其余各个顶点的最短路径
for(i=1;i<=n;i++)
printf("%d ",dis[i]);
getchar(); getchar();
return 0;
}
补充
邻接表存储图:
n个顶点,m条边。
数组实现邻接表。对每一条边进行1-m编号。用u,v,w三个数组来记录每条边的信息,即u[i],v[i],w[i]表示第i条边是从第 u[i]号顶点到v[i]号顶点且权值为w[i].
first数组的1-n号单元格分别用来存储1-n号顶点的第一条边的编号,初始的时候因为没有边加入所有都是-1.即first[u[i]]保存顶点u[i]的第一条边的编号,next[i]存储“编号为i的边”的“下一条边”的编号。
接下来遍历边。遍历1号订单的每一条边。
在找到1号顶点的第一条边之后,剩下得边都可以在next数组中一次找到
此时遍历某个顶点的边的时候的遍历顺序正好与读入时候的顺序相反。因为在为每个顶点插入边的时候都是直接插入“链表”的首部而不是尾部。
用临接表来存储图的时间空间复杂度是O(M),遍历每一条边的时间复杂度也是O(M),如果一个图是稀疏图的话,M要远小于N^2,因此稀疏图选用邻接表来存储比用邻接矩阵来存储好很多。