Bellman-Ford算法证明与代码示例

前导

对于一个有权图 G ( V , E ) G(V,E) G(V,E),记 w ( v i − 1 , v i ) w(v_{i-1},v_i) w(vi1,vi) 边 v i − 1 , v i 边v_{i-1},v_i vi1,vi之间的权值,路径 p = ( v 0 , v 1 , . . . , v k ) p=(v_0,v_1,...,v_k) p=(v0,v1,...,vk)的权是指其所组成边的所有权值之和, w ( p ) = ∑ i = 1 k w ( v i − 1 , v i ) w(p)=\sum\limits_{i=1}^{k} w(v_{i-1},v_i) w(p)=i=1kw(vi1,vi)
定义从 u u u v v v间的最短路径的权值为: δ ( u , v ) = { m i n { w ( p ) : u → p v } 如 果 存 在 着 一 条 从 u 到 v 的 路 径 ∞ 否 则 \delta(u,v)= \left\{\begin{matrix} && min\left \{ w(p):u\overset{p}{\rightarrow}v \right \} \qquad\qquad如果存在着一条从u到v的路径\\ && \infty \qquad否则 \end{matrix}\right. δ(u,v)={min{w(p):upv}uv

从顶点u到顶点v的最短路径定义为权 w ( p ) = δ ( u , v ) w(p)=\delta(u,v) w(p)=δ(u,v)的任何路径

----《算法导论》

Bellman-Ford算法

参考:https://www.jianshu.com/p/b876fe9b2338
目的: 解决含负权边的单源最短路径问题,同时可以判断是否存在负权回路

负环,又叫负权回路,负权环,指的是一个图中存在一个环,里面包含的边的边权总和<0。在存在负环的图中,是求不出最短路径的,因为只要在这个环上不停的兜圈子,最短路径就会无限小。

时间复杂度:O(VE),空间复杂度O(E),( V为顶点数,E为边数)

算法描述:
(1)初始化:将除起点s外所有顶点的距离数组置无穷大 d[v] = INF, d[s] = 0
(2)迭代:遍历图中的每条边,对边的两个顶点分别进行一次松弛操作,直到没有点能被再次松弛
(3)判断负圈:如果迭代超过V-1次,则存在负圈
在这里插入图片描述
松弛: 以 w ( u , v ) w(u,v) w(u,v)表示顶点 u u u出发到顶点 v v v的边的权值,以 d [ v ] d[v] d[v]表示当前从起点 s s s到顶点 v v v的路径权值
若存在边 w ( u , v ) w(u,v) w(u,v),使得:
d [ v ] > d [ u ] + w ( u , v ) d[v]>d[u]+w(u,v) d[v]>d[u]+w(u,v)
则更新 d [ v ] d[v] d[v]值:
d [ v ] = d [ u ] + w ( u , v ) d[v]=d[u]+w(u,v) d[v]=d[u]+w(u,v)
松弛就是经过某个顶点或者某条边,可以缩短起点到终点的路径权值。
在这里插入图片描述

负权环判定:

负环,又叫负权回路,负权环,指的是一个图中存在一个环,里面包含的边的边权总和<0。

因为负权环可以无限制的进行松弛,所以如果发现第|V|次操作仍有松弛发生,就一定存在负权环

Bellman-Ford算法证明

参考https://www.jianshu.com/p/b876fe9b2338
问题: 为啥最多迭代 V − 1 V-1 V1次就不再存在能够被松弛的点,也就是找到了单源最短路径(不含负权环时)

先引出如下引理,均来自《算法导论》
在这里插入图片描述
最短路径及松弛的相关定理在这里插入图片描述

路径松弛性质:

对于图 G ( V , E ) G(V,E) G(V,E),如果 p = ( v 0 , v 1 , v 2 , … , v k ) p=(v_0,v_1,v_2,…,v_k) p=(v0,v1,v2,,vk) s = v 0 s=v_0 s=v0 v k v_k vk的最短路径,而且按照 ( v 0 , v 1 ) ( v 1 , v 2 ) , … , ( v k − 1 , v k ) (v_0,v_1)(v_1,v_2),…,(v_k-1,v_k) (v0,v1)(v1,v2),,(vk1,vk)的顺序进行松弛,那么 d [ v k ] = δ ( s , v k ) d[v_k]=δ(s,v_k) d[vk]=δ(s,vk)这个性质的保持并不受到其他松弛操作的影响,即使他们与p的边上的松弛操作混合在一起也是一样的。

通过归纳法可证:对于 v 0 到 v k 的 最 短 路 径 v_0到v_k的最短路径 v0vkp , 有 ,有 d[v_0]=d[s]=0=δ(s,s) , 假 设 , 假设 d[v_{i-1}]=δ(s,v_{i-1}) , 并 检 查 边 ,并检查边 (v_{i-1},v_i) 的 松 弛 。 根 据 收 敛 性 质 , 在 这 次 松 弛 后 即 有 的松弛。根据收敛性质,在这次松弛后即有 d[v_i]=δ(s,v_i)$,且不会在后继操作变化!

解释一下为啥这个性质的保持并不受到其他松弛操作的影响,即使他们与p的边上的松弛操作混合在一起也是一样的。

  • 假设 d [ v i − 1 ] = δ ( s , v i − 1 ) d[v_{i-1}]=δ(s,v_{i-1}) d[vi1]=δ(s,vi1);

  • 接下来先松弛了不在路径 p p p上的另一条边 ( v t , v i ) (v_t,v_i) (vt,vi),此时根据上界性质

  • d [ v i ] > = δ ( s , v i ) d[v_i]>=\delta(s,v_i) d[vi]>=δ(s,vi)为啥不是直接 d [ v i ] = δ ( s , v i ) d[v_i]=\delta(s,v_i) d[vi]=δ(s,vi),因为边 ( v t , v i ) (v_t,v_i) (vt,vi)并不是最短路径p上的边,所以松弛边 ( v t , v i ) (v_t,v_i) (vt,vi) d [ v i ] 并 不 是 δ ( s , v i ) d[v_i]并不是\delta(s,v_i) d[vi]δ(s,vi));

  • 然后在松弛边 ( v i − 1 , v i ) (v_{i-1},v_i) (vi1,vi),这里的边 ( v i − 1 , v i ) (v_{i-1},v_i) (vi1,vi)是最短路径p上的边,所以一定会发生松弛, d [ v i ] > δ ( s , v i − 1 ) + w ( v i − 1 , v i ) d[v_i]>\delta(s,v_{i-1})+w(v_{i-1},v_i) d[vi]>δ(s,vi1)+w(vi1,vi),松弛过后 d [ v i ] = δ ( s , v i − 1 ) + w ( v i − 1 , v i ) = δ ( s , v i ) d[v_i]=\delta(s,v_{i-1})+w(v_{i-1},v_i)=\delta(s,v_i) d[vi]=δ(s,vi1)+w(vi1,vi)=δ(s,vi)

这表明,如果 p = ( v 0 , v 1 , v 2 , … , v k ) p=(v_0,v_1,v_2,…,v_k) p=(v0,v1,v2,,vk) s = v 0 s=v_0 s=v0 v k v_k vk的最短路径,而且按照 ( v 0 , v 1 ) ( v 1 , v 2 ) , … , ( v k − 1 , v k ) \mathbf{(v_0,v_1)(v_1,v_2),…,(v_k-1,v_k)} (v0,v1)(v1,v2),,(vk1,vk)的顺序进行松弛,不论我们在其中穿插了多少不在路径p上的其它边,

例如按照 ( v 0 , v 1 ) ( v t 0 , v t 1 ) ( v 1 , v 2 ) , … , ( v k − 1 , v k ) 的 顺 序 进 行 松 弛 \mathbf{(v_0,v_1)}(v_{t_0},v_{t_1})\mathbf{(v_1,v_2)},\mathbf{…,(v_{k-1},v_k)}的顺序进行松弛 (v0,v1)(vt0,vt1)(v1,v2),,(vk1,vk),
或者按照 ( v 1 , v 2 ) ( v 0 , v 1 ) ( v k − 1 , v k ) ( v 1 , v 2 ) , … , ( v k − 1 , v k ) 的 顺 序 进 行 松 弛 (v_1,v_2)\mathbf{(v_0,v_1)}(v_{k-1},v_k)\mathbf{(v_1,v_2),…,(v_{k-1},v_k)}的顺序进行松弛 (v1,v2)(v0,v1)(vk1,vk)(v1,v2),,(vk1,vk)

不论你在上面的 ( v 0 , v 1 ) ( v 1 , v 2 ) , … , ( v k − 1 , v k ) \mathbf{(v_0,v_1)(v_1,v_2),…,(v_{k-1},v_k)} (v0,v1)(v1,v2),,(vk1,vk)的顺序中插入了啥边,最后的结果都是 d [ v k ] = δ ( s , v k ) d[v_k]=δ(s,v_k) d[vk]=δ(s,vk)
注:因为证明的前提 p = ( v 0 , v 1 , v 2 , … , v k ) p=(v_0,v_1,v_2,…,v_k) p=(v0,v1,v2,,vk) s = v 0 s=v_0 s=v0 v k v_k vk的最短路径。

举例如下图,我们已知 p ( v 0 , v 1 , v 4 ) p(v_0,v_1,v_4) p(v0,v1,v4) s = v 0 到 v 4 的 最 短 路 径 , 且 δ ( s , v 4 ) = − 3 s=v_0到v_4的最短路径,且\delta(s,v_4)=-3 s=v0v4,δ(s,v4)=3
我们先初始化dis数组为【0,inf ,inf,inf,inf,inf】
在这里插入图片描述
在这里插入图片描述

  1. 我们按照边 ( v 0 , v 1 ) , ( v 1 , v 4 ) , . . . , \mathbf{(v_0,v_1),(v_1,v_4)},... , v0,v1,(v1,v4)...,的顺序松弛,可得dis[v_4]=-3;
  2. 我们按照边 ( v 0 , v 1 ) , ( v 3 , v 4 ) , ( v 0 , v 2 ) , . . . , ( v 1 , v 4 ) \mathbf{(v_0,v_1)},(v_3,v_4),(v_0,v_2),...,\mathbf{(v_1,v_4)} v0,v1,(v3,v4),(v0,v2),...,(v1,v4)的顺序松弛,可得dis[v_4]=-3;
  3. 我们按照边 ( v 1 , v 4 ) ( v 0 , v 1 ) , . . . , ( v 1 , v 4 ) (v_1,v_4)\mathbf{(v_0,v_1)},...,\mathbf{(v_1,v_4)} (v1,v4)v0,v1,...,(v1,v4)的顺序松弛,可得dis[v_4]=-3;
  4. 其实不论在边序列 ( v 0 , v 1 ) , ( v 1 , v 4 ) \mathbf{(v_0,v_1),(v_1,v_4)} v0,v1,(v1,v4)中穿插啥,松弛过后,dis[v_4]=-3;这也是前面的路径松弛性质。

为啥Bellman-Ford算法最多迭代 V − 1 V-1 V1

有了这个路径松弛性质,我们接下来再来说明为啥Bellman-Ford算法最多迭代 V − 1 V-1 V1次,就能找到图中源点到其它所有点的最短路径(单源最短路径),当然,这里先假设图中是没有负权环存在的。

Bellman-Ford算法里对图中所有边执行一次松弛函数作为一次迭代,一共迭代 V − 1 V-1 V1次,可以确保计算出起点到每个顶点的最短距离。

先举一个简单的例子,源点 v 0 v_0 v0
在这里插入图片描述
初始化dis数组为【0,inf ,inf,inf,inf】

  • 最好情况:
    所有边的顺序为 ( v 0 , v 1 ) ( v 1 , v 2 ) ( v 2 , v 3 ) ( v 3 , v 4 ) (v_0,v_1)(v_1,v_2)(v_2,v_3)(v_3,v_4) (v0,v1)(v1,v2)(v2,v3)(v3,v4), 迭代1次,dis数组就变为【0,2,0,-1,4】,即找到了源点为 v 0 v_0 v0的单源最短路径。
  • 最坏情况:
    所有边的顺序为 ( v 3 , v 4 ) ( v 2 , v 3 ) ( v 1 , v 2 ) ( v 0 , v 1 ) (v_3,v_4)(v_2,v_3)(v_1,v_2)(v_0,v_1) (v3,v4)(v2,v3)(v1,v2)(v0,v1), 迭代4次,才能找到源点为 v 0 v_0 v0的单源最短路径。
    第1次迭代:dis数组就变为【0,2,inf,inf,inf】
    第2次迭代:dis数组就变为【0,2,0,inf,inf】
    第3次迭代:dis数组就变为【0,2,0,-1,inf】
    第4次迭代:dis数组就变为【0,2,0,-1,4】

可以看出最坏迭代V-1次(V个顶点,最短路径p最多含有V-1条边),就能找到源点为 v 0 v_0 v0的单源最短路径(V为图的顶点数)

再举一个复杂点的例子,源点 v 0 v_0 v0
在这里插入图片描述
初始化dis数组为【0,inf ,inf,inf,inf】
我们已知 d [ v 0 ] = δ ( s , s ) = 0 d[v_0]=\delta(s,s)=0 d[v0]=δ(s,s)=0

对于源点 v 0 v_0 v0到图中其它点的最短路径p,则p中含有的边最少为1,最多为V-1。
因为V个顶点,最短路径p最少含有1条边,最多含有V-1条边。

对应到上图中,

  • 即源点 v 0 到 v 1 v_0到v_1 v0v1最短路径只含1条边( p ( v 0 , v 1 ) p(v_0,v_1) p(v0,v1))、源点 v 0 到 v 2 v_0到v_2 v0v2的最短路径只含1条边( p ( v 0 , v 2 ) p(v_0,v_2) p(v0,v2)),
  • 即源点 v 0 到 v 3 v_0到v_3 v0v3最短路径只含2条边( p ( v 0 , v 1 , v 3 ) p(v_0,v_1,v_3) p(v0,v1,v3)、源点 v 0 到 v 4 v_0到v_4 v0v4的最短路径只含2条边( p ( v 0 , v 1 , v 4 ) p(v_0,v_1,v_4) p(v0,v1,v4)
  • 在上图中,最短路径最多只含2条边,也就是说不论你所有边的顺序是啥样的,Bellman-Ford算法执行2迭代就找到了源点为 v 0 v_0 v0到其它所有点的最短路径

不信的话,对于上面的图,我们举个例子:初始化dis数组为【0,inf ,inf,inf,inf】

  • 所有边的顺序为 ( v 0 , v 1 ) ( v 0 , v 2 ) ( v 1 , v 3 ) ( v 1 , v 4 ) ( v 2 , v 4 ) ( v 3 , v 4 ) (v_0,v_1)(v_0,v_2)(v_1,v_3)(v_1,v_4)(v_2,v_4)(v_3,v_4) (v0,v1)(v0,v2)(v1,v3)(v1,v4)(v2,v4)(v3,v4)
    – 迭代1次,dis数组为【0,2,3,-1,-3】
  • 所有边的顺序为 ( v 3 , v 4 ) ( v 2 , v 4 ) ( v 1 , v 4 ) ( v 1 , v 3 ) ( v 0 , v 2 ) ( v 0 , v 1 ) (v_3,v_4)(v_2,v_4)(v_1,v_4)(v_1,v_3)(v_0,v_2)(v_0,v_1) (v3,v4)(v2,v4)(v1,v4)(v1,v3)(v0,v2)(v0,v1)
    – 迭代1次,dis数组为【0,2,3,inf,inf】
    – 迭代2次,dis数组为【0,2,3,-1,-3】
  • 这里不论所有边的顺序你修改成啥,Bellman-Ford算法执行2迭代就找到了源点为 v 0 v_0 v0到其它所有点的最短路径

这里由以下4点,就可以证明Bellman-Ford算法最多迭代 V − 1 V-1 V1

  1. 对于源点 v 0 v_0 v0到图中其它点的最短路径p,则p中含有的边最少为1,最多为V-1(因为V个顶点,最短路径p最少含有1条边,最多含有V-1条边)
  2. 最极端情况下,最短路径含有V-1条边。
  3. 对于含V-1条边的最短路径 p ( v 0 , v 1 , v 2 , . . . , v v − 2 , v v − 1 ) p(v_0,v_1,v_2,...,v_{v-2},v_{v-1}) p(v0,v1,v2,...,vv2,vv1),我们最好可能1次迭代,就确定了其最短路径的权值;最差迭代V-1次,就确定了最短路径的权值(因为迭代V-1次,松弛边序列中一定会出现 . . . , ( v 0 , v 1 ) , . . . , ( v 1 , v 2 ) , … , ( v v − 2 , v v − 1 ) , . . . ...,(v_0,v_1),...,(v_1,v_2),…,(v_{v-2},v_{v-1}),... ...,(v0,v1),...,(v1,v2),,(vv2,vv1),...这种边序列,不论里面穿插了多少其它边,都不影响路径松弛性质)。

路径松弛性质
对于图 G ( V , E ) G(V,E) G(V,E),如果 p = ( v 0 , v 1 , v 2 , … , v k ) p=(v_0,v_1,v_2,…,v_k) p=(v0,v1,v2,,vk) s = v 0 s=v_0 s=v0 v k v_k vk的最短路径,而且按照 ( v 0 , v 1 ) ( v 1 , v 2 ) , … , ( v k − 1 , v k ) (v_0,v_1)(v_1,v_2),…,(v_k-1,v_k) (v0,v1)(v1,v2),,(vk1,vk)的顺序进行松弛,那么 d [ v k ] = δ ( s , v k ) d[v_k]=δ(s,v_k) d[vk]=δ(s,vk)这个性质的保持并不受到其他松弛操作的影响,即使他们与p的边上的松弛操作混合在一起也是一样的

  1. 这里的每次迭代都遍历图中的所有边(但这些边的顺序是随便的),对每条边都进行松弛操作。

这里给出几句话帮助理解

  • 迭代的实际意义:第k次迭代中,我们找到了经历了k条边的最短路径。
  • “没有点能够被松弛”时,迭代结束

这里还给出一段描述性的证明:

  • 首先指出,图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。
  • 其次,从源点s可达的所有顶点,如果存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。
  • 在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;
  • 对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径
  • ……
  • 因为最短路径最多只包含|v|-1 条边,所以,只需要循环|v|-1 次。

这里还给出《算法导论中的证明》:
在这里插入图片描述

Bellman-Ford算法代码示例

#include<iostream>  
#include<cstdio>  
using namespace std;  
#define MAX 0x3f3f3f3f  
#define N 1010  

int nodenum, edgenum, original; //点,边,起点  
typedef struct Edge //边  
{  
    int u, v;  
    int cost;  
}Edge;  
   
Edge edge[N];  
int dis[N], pre[N];  
   
bool Bellman_Ford()  
{  
    for(int i = 1; i <= nodenum; ++i) //初始化  
        dis[i] = (i == original ? 0 : MAX);  
    for(int i = 1; i <= nodenum - 1; ++i)  
        for(int j = 1; j <= edgenum; ++j)  
            if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛
            {  
                dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;  
                pre[edge[j].v] = edge[j].u;  
            }  
    bool flag = 1; //判断是否含有负权回路  
    for(int i = 1; i <= edgenum; ++i)  
        if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)  
        {  
            flag = 0;  
            break;  
        }  
        return flag;  
}  

void print_path(int root) //打印最短路的路径(反向)  
{  
    while(root != pre[root]) //前驱  
    {  
        printf("%d-->", root);  
        root = pre[root];  
    }  
    if(root == pre[root])  
        printf("%d\n", root);  
}  


int main()  
{  
    scanf("%d%d%d", &nodenum, &edgenum, &original);  
    pre[original] = original;  
    for(int i = 1; i <= edgenum; ++i)  
    {  
        scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);  
    }  
    if(Bellman_Ford())  
        for(int i = 1; i <= nodenum; ++i) //每个点最短路  
        {  
            printf("%d\n", dis[i]);  
            printf("Path:");  
            print_path(i);  
        }  
    else  
        printf("have negative circle\n");  
    return 0;  

}  

SPFA(Bellman-Ford算法改进版本)

SPFA算法是1994年西安交通大学段凡丁提出。
思想: 松弛操作必定只会发生在最短路径前导节点松弛成功过的节点上,用一个队列记录松弛过的节点,可以避免了冗余计算。
原文中提出该算法的复杂度为O(k|E|)},k为每个节点进入Queue的次数,且k一般<=2,但此处的复杂度证明是有问题的,其实SPFA的最坏情况应该是O(VE).

SPFA可以处理负权边和负权环,关于SPFA判负环
参考https://blog.csdn.net/forever_dreams/article/details/81161527

SPFA代码示例

int SPFA(int s) {
      queue<int> q; 
      bool inq[maxn] = {false}; 
      for(int i = 1; i <= N; i++) dis[i] = 2147483647; 
      dis[s] = 0; 
      q.push(s); inq[s] = true; 
      while(!q.empty()) { 
          int x = q.front(); q.pop(); 
          inq[x] = false;
          for(int i = front[x]; i !=0 ; i = e[i].next) {
              int k = e[i].v;
              if(dis[k] > dis[x] + e[i].w) {
                  dis[k] = dis[x] + e[i].w;
                  if(!inq[k]) {
                      inq[k] = true;
                      q.push(k);
                  }
              }
          }
      }
      for(int i =  1; i <= N; i++) cout << dis[i] << ' ';
      cout << endl;
      return 0;
  }
  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值