Bellman-Ford算法证明与代码示例
前导
对于一个有权图
G
(
V
,
E
)
G(V,E)
G(V,E),记
w
(
v
i
−
1
,
v
i
)
w(v_{i-1},v_i)
w(vi−1,vi)为
边
v
i
−
1
,
v
i
边v_{i-1},v_i
边vi−1,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=1∑kw(vi−1,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):u→pv}如果存在着一条从u到v的路径∞否则
从顶点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
V−1次就不再存在能够被松弛的点,也就是找到了单源最短路径(不含负权环时)
先引出如下引理,均来自《算法导论》
最短路径及松弛的相关定理
路径松弛性质:
对于图 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),…,(vk−1,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的最短路径 v0到vk的最短路径p , 有 ,有 ,有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[vi−1]=δ(s,vi−1);
-
接下来先松弛了不在路径 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) (vi−1,vi),这里的边 ( v i − 1 , v i ) (v_{i-1},v_i) (vi−1,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,vi−1)+w(vi−1,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,vi−1)+w(vi−1,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),…,(vk−1,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),…,(vk−1,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)(vk−1,vk)(v1,v2),…,(vk−1,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),…,(vk−1,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=v0到v4的最短路径,且δ(s,v4)=−3
我们先初始化dis数组为【0,inf ,inf,inf,inf,inf】
- 我们按照边 ( 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;
- 我们按照边 ( 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;
- 我们按照边 ( 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;
- 其实不论在边序列 ( 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 V−1次
有了这个路径松弛性质,我们接下来再来说明为啥Bellman-Ford算法最多迭代 V − 1 V-1 V−1次,就能找到图中源点到其它所有点的最短路径(单源最短路径),当然,这里先假设图中是没有负权环存在的。
Bellman-Ford算法里对图中所有边执行一次松弛函数作为一次迭代,一共迭代 V − 1 V-1 V−1次,可以确保计算出起点到每个顶点的最短距离。
先举一个简单的例子,源点
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 v0到v1最短路径只含1条边( p ( v 0 , v 1 ) p(v_0,v_1) p(v0,v1))、源点 v 0 到 v 2 v_0到v_2 v0到v2的最短路径只含1条边( p ( v 0 , v 2 ) p(v_0,v_2) p(v0,v2)),
- 即源点 v 0 到 v 3 v_0到v_3 v0到v3最短路径只含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 v0到v4的最短路径只含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 V−1次
- 对于源点 v 0 v_0 v0到图中其它点的最短路径p,则p中含有的边最少为1,最多为V-1(因为V个顶点,最短路径p最少含有1条边,最多含有V-1条边)
- 最极端情况下,最短路径含有V-1条边。
- 对于含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,...,vv−2,vv−1),我们最好可能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),…,(vv−2,vv−1),...这种边序列,不论里面穿插了多少其它边,都不影响路径松弛性质)。
路径松弛性质
对于图 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),…,(vk−1,vk)的顺序进行松弛,那么 d [ v k ] = δ ( s , v k ) d[v_k]=δ(s,v_k) d[vk]=δ(s,vk)。这个性质的保持并不受到其他松弛操作的影响,即使他们与p的边上的松弛操作混合在一起也是一样的。
- 这里的每次迭代都遍历图中的所有边(但这些边的顺序是随便的),对每条边都进行松弛操作。
这里给出几句话帮助理解
- 迭代的实际意义:第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;
}