算法入门之完美单源最短路径:Bellman-Ford(贝尔曼-福特)算法

Posted Justin的后端书架

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法入门之完美单源最短路径:Bellman-Ford(贝尔曼-福特)算法相关的知识,希望对你有一定的参考价值。

上节我们讲到了Dijkstra算法求单源最短路径,还记得当时我们提到一点关于负权边的问题吗?上节我们说了Dijkstra要求图中不能存在负权边,那么这个对于有负权边的图,我们要怎么求单源最短路径呢?今天我们就介绍另一种算法,也被称为比较完美的求最短路径算法:Bellman-Ford算法。


场景:

思路分析

  • 第一步:初始化所有点。每一个点保存一个值,表示从原点到达这个点的距离,将原点的值设为0,其它的点的值设为无穷大;

  • 第二,进行循环,循环下标为从1到n-1(n等于图中点的个数)。在循环内部,遍历所有的边,进行松弛计算;

  • 第三,遍历途中所有的边,判断是否存在源点1到某点y的距离大于源点到x的距离加上x到y的路径,存在则拿1-x的距离+x到y边的值替换1到y的距离;


代码如下(Bellman-Ford算法求单源最短路径)


public class BellmanFord {
   
// 贝尔曼-福特算法求负权单源最短路径
   
public static void main(String[] args) {
       
// 定义dis数组记录源点到其他点的最短路径
       
int[] dis = new int[10];
       int
n = 5; //顶点的个数
       
int m = 7;// 边的个数
       
int max = 99999999;
       
//xyz原来存顶点到顶点的距离数据
       
int[] x =new int[10];
       int
[] y =new int[10];
       int
[] z =new int[10];
       
//存储边的信息
       
x[1]=2;y[1]=3;z[1]=2; //第一条边
       
x[2]=1;y[2]=2;z[2]=-3; //第二条边
       
x[3]=5;y[3]=1;z[3]=5; //第三条边
       
x[4]=4;y[4]=5;z[4]=2; //第四条边
       
x[5]=3;y[5]=4;z[5]=3; //第五条边
       
x[6]=2;y[6]=5;z[6]=2; //第六条边
       
x[7]=2;y[7]=4;z[7]=5; //第六条边
       //初始化dis数组
       
for (int i = 1; i <=n ; i++) {
           dis[i]
= max;
       
}
       dis[
1]=0;//初始化源点
       // 定义一个数组备份当前dis数组
       
int[] bak = new int[10];
       
// 遍历n个顶点,从源点开始
       
for (int i = 1; i <= n - 1; i++) {
           
// 备份dis数组
           
for (int j = 1; j <= n ; j++) {
               bak[j]
= dis[j];
           
}
           
//每个顶点都对m条边做一次松弛
           
for (int j = 1; j <= m; j++) {
               
if (dis[y[j]] > dis[x[j]] + z[j]) {
                   dis[y[j]]
= dis[x[j]] + z[j];
               
}
           }
           
int check =0;
           for
(int j = 1; j <= n ; j++) {
               
//如果当前dis数组中和备份数组不一样
               // 就打上标记1,
               
if(bak[j] != dis[j]){
                   check
= 1;
                   break;
               
}
           }
           
// 如果标记为0,则代表dis没有发生变化
           // 说明我们在未遍历到n-1时
           // 已经求得最短路,跳出循环
           
if(check == 0){
               
break;
           
}
       }
       
// 检查是否存在最短路径负权回路
       
int tag = 0;
       for
(int i = 1; i <=n ; i++) {
           
if(dis[y[i]] > dis[x[i]] + z[i]){
               tag
= 1;
           
}
       }
       
if(tag == 1){
           
System.out.println("此图包含负权回路");
       
}else {
           
System.out.println("此图不包含负权回路");
       
}
       
for (int i = 1; i <=n ; i++) {
   
System.out.println("1号点到"+i+"号顶点的最短路径为:"+dis[i]+"");
       
}
   }
}
结果:
   此图不包含负权回路
   1号点到1号顶点的最短路径为:0
   1号点到2号顶点的最短路径为:-3
   1号点到3号顶点的最短路径为:-1
   1号点到4号顶点的最短路径为:2
   1号点到5号顶点的最短路径为:-1

分析

因为最短路径最多有n-1条边,因此该算法最多有n-1个阶段,循环n-1次,每次循环里面,我们队每一条边都要进行一次松弛操作,每进行一次松弛,就会有些顶点已经求得了最短路径,此后的操作这些顶点的最短路径都不会变,所以我们没必要再去判断是否需要松弛,因此我们通过备份一个源点到顶点距离的数组bak[]来判断是否需要提前跳出循环,在i个循环结束后,我们就已经找到了从源点最多经过i条边到达各个顶点的最短路径; 直到n-1结束,我们就得出了,最多经过n-1条边后源点到各个顶点的最短路径;


时间复杂度:

上述代码,我们通过定义一个bak数组来优化了循环次数,从来不需要把每条边都遍历完即可用提前跳出循环,因为源点到每个点的最短路径不是需要遍历完所有边才得到,有时候中中途就已经提前得到了最短路径;假设没有这段优化,我们的时间复杂度是O((i-1)*j=O(N²);


思考:

上面我们提到,在每次松弛操作后,就会有一些顶点已经求得了最短路径,此后操作都不会影响这些顶点的最短路径,那么我们是不是可以只需要关注最短路径发生变化的顶点就可以了呢?


通过队列优化Bellman-Ford算法的时间复杂度


public class BellmanFord2 {
   
// Bellman-Ford算法时间复杂度优化
   
public static void main(String[] args) {
       
// 定义dis数组记录源点到其他点的最短路径
       
int[] dis = new int[10];
       int
n = 5; //顶点的个数
       
int m = 7;// 边的个数
       
int max = 99999999;
       int
[] frist=new int[6];
       for
(int i = 1; i <=n ; i++) {
           frist[i]
= -1;
       
}
       frist[
0] = 0;
       int
[] next=new int[8];
       
next[0] = 0;
       int
[] book = new int[6];
       for
(int i = 0; i <=n ; i++) {
           book[i]
= 0;
       
}
       book[
1]=1;
       int
[] queue = new int[100];
       
queue[0] = 0;
       
//xyz原来存顶点到顶点的距离数据
       
int[] x =new int[10];
       int
[] y =new int[10];
       int
[] z =new int[10];
       
//存储边的信息
       
x[1]=2;y[1]=3;z[1]=2; //第一条边
       
x[2]=1;y[2]=2;z[2]=-3; //第二条边
       
x[3]=5;y[3]=1;z[3]=5; //第三条边
       
x[4]=4;y[4]=5;z[4]=2; //第四条边
       
x[5]=3;y[5]=4;z[5]=3; //第五条边
       
x[6]=2;y[6]=5;z[6]=2; //第六条边
       
x[7]=2;y[7]=4;z[7]=5; //第六条边
       
for (int i = 1; i <=m ; i++) {
           next[i]
=frist[x[i]];
           
frist[x[i]] = i;
       
}
       
//初始化dis数组
       
for (int i = 1; i <=n ; i++) {
           dis[i]
= max;
       
}
       dis[
1]=0;//初始化源点
       
int head =1;//头
       
int tail =1;// 尾
       // 1号点先入队
       
queue[tail] =1;
       
// 尾部+1
       
tail++;
       int
k ;
       
// 循环直到queue中没有顶点了
       
while (head < tail){
           k
= frist[queue[head]];
           while
(k!=-1){
               
//判断松弛结果,是否存在一条边路径小于当前距离
               
if(dis[y[k]] > dis[x[k]] + z[k]){
                   dis[y[k]]
= dis[x[k]] + z[k];
                   
// 判断当前点是否已经标记
                   
if(book[y[k]] == 0){
                       
//入队,存放去队尾
                       
queue[tail]=y[k];
                       
tail++;
                       
// 并打上标记
                       
book[y[k]] = 1;
                   
}
               }
               k
=next[k];
           
}
         
// 当当前点全部计算完成 出列
         
book[queue[head]] = 0;
           
head++;
       
}
       
for (int i = 1; i <=n ; i++) {
   
System.out.println("1号点到"+i+"号顶点的最短路径为:"+dis[i]+"");
       
}
   }
}
结果:
   1号点到1号顶点的最短路径为:0
   1号点到2号顶点的最短路径为:-3
   1号点到3号顶点的最短路径为:-1
   1号点到4号顶点的最短路径为:2
   1号点到5号顶点的最短路径为:-1

分析

  • 初始化时,将源点加入队列,每次从头取出一个顶点,并且与其相邻的所有顶点进行松弛,如果松弛成功,即存在更短的路径,则判断该顶点是都在队列中,如果不存在,则加入队列;

  • 当前顶点处理结束后就出列,并对下一个新的头顶点进行上述操作,直到队列为空,即所有的顶点都不会松弛成功,即不存在更短的路径了

  • 如果某个顶点进入队列的次数超过n从,则说明这个图存在负权环

  • 时间复杂度:最差的情况是O(N²),<=O(N²)


总结:用队列优化的Bellman-Ford算法的关键在于,只有某些在上一次松弛中改变了最短路径的顶点,才会对他们相邻连接顶点的最短路径发生影响,通过将这些松弛成功的点存放在一个队列,然后对这个队列中的点再去进行处理,这就降低了时间复杂度


相关文章



以上是关于算法入门之完美单源最短路径:Bellman-Ford(贝尔曼-福特)算法的主要内容,如果未能解决你的问题,请参考以下文章

算法入门之单源最短路径算法:Dijkstra(迪杰斯特拉)算法

Spark系列 SparkGraphX下的Pregel方法----完美解决单源最短路径的应用算法

算法之单源最短路径

Bellman-ford 单源最短路径算法

贪心算法—单源最短路径

图文解析 Dijkstra单源最短路径算法