算法背景
Bellman-Ford 算法是一般情况下求单源最短路径的算法,边的权重可以为负。也可以用来判断从一个源点s出发,能否到达负环。时间复杂度为O(n^2),n为图的边数。该算法相当于单元最短路的暴力搜索法。
算法实现步骤
Bellamn-Ford算法维护的是一个数组,该数组记录了每个节点到源节点的距离。然后通过一系列的步骤不断更新这个数组,直到所有值不在改变,也就是距离到达最小值。这些操作中包含图论算法中的一些基本的操作,这里先了解一下:
Initialize-Single-Source(G,s)
#该操作就是一个初始化操作,该操作将图中所有节点到源节点s的权值初始化为正无穷,再将源节点到源
节点的权值设为0;
Relax(u,v,w)
#该操作是最短路算法的核心操作,该操作作用是降低最短路径的估计值v.d(就是降低v节点到源节点的
权值)。更具体一点的步骤是 将(v节点到源节点的权值) v.d 与 (v节点的前置节点)u.d + (u,v边
的权值) w (u,v),相比较,若前者大于后者,则将前者的值更新为后者。
Bellman-Ford 算法的大致步骤是:首先建好图,将每个节点到源节点s的距离初始化为无穷。然后循环 v-1 次,v是图的边数,每一次遍历所有的边edg(u,v),在遍历边的时候对这条边进行松弛操作(Relax)。循环结束后每个点到源节点的最小权值就已经更新完成了。
代码实现
- 伪代码
完整伪代码:
Bellman-Ford(G,w,s)
{
//初始化
Initialize-Single-Source(G,s)
//暴力搜索最短路径,更新每个节点到源节点s的最短路径
for i=1 to |G,V| - 1
for each eadge(u,v) in G.E
Relax(u,v,w)
//判断从源节点s能到达的路径上是否有负环
for each edge(u,v) in G.E
if v.d > u.d + w(u,v)
return false
return true
}
1、G表示图,G的两个属性E,V分别表示图的边和节点
2、|G,V|表示途中所有边的边数
3、w是权重函数,w(a,b)表示a,b边的权重
4、s是源节点
5、节点u包含属性d,u.d 表示节点u到源节点s的最短路径估计值(到源节点的权重值)
6、Initialize-Single-Source(G,s)伪代码如下:
Initialize-Single-Source(G,s)
{
for each vertex v in G.V
v.d=max
s.d=0
}
7、Relax(u,v,w)伪代码如下
Relax(u,v,w)
{
for each edge(u,v) in G.E
if v.d > u.d + w(u,v)
v.d = u.d + w(u,v)
}
- 吸佳佳代码
#include<iostream>
#include<cstring>
using namespace std;
const int N = 100010;
int v;//题目中输入的边的条数
int dist[N];//dist[i]=w 表示节点节点i到源点s的权值为w
struct Edge
{
int a,b,c;//表示a指向b的边的权重为c
}edges[N]//记录了图中的所有边,这些边是无序的
bool Bellman-Ford(int s)
{
//初始化
memset(dist,0x3f,sizeof dist);
dist[s]=0;
//暴力搜索,更新dist数组,若只是求最短路到这一步就可以结束了,改一下函数返回类型就行了
for( int i=0 ; i<v-1 ; i++ )
{
for( int j=0 ; j<v ; j++ )//枚举每一条边
{
int a=edges[i].a , b=edges[i].b , c=edges[i].c;
if( dist[b] > dist[a] + c )
dist[b] = dist[a] + c;
}
}
//查看路径上是否有负环
for( int i=0 ; i<v ; i++ )
{
int a=edges[i].a , b=edges[i].b , c=edges[i].c;
if( dist[b] > dist[a] + c )return true;//路径上存在负环
}
return false;
}
Bellman-Ford算法寻找最短路可行性证明
为了证明Bellman-Ford算法的可行性,我们需要先了解涉及到的一些概念和性质:
-
松弛操作:松弛是唯一导致最短路径估计值和前驱节点发生变化的操作。
-
路径松弛性质:对于某些节点u,v,如果p< v0 , v1 , v2 , ... , vk > 是从源节点s=v0到节点vk的一条最短路径,并且我们对p的边进行松弛的次序为 ( v0 , v1 ) ( v1 , v2 ) , ... , ( vk-1 , vk ) ,则vk.d=δ(s,vk)( δ(s,vk)表示s到vk的最小权值 )。这个性质的保持并不受到其他松弛操作的影响,即使他们与p的边上的松弛操作混合在一起也是一样的。
-
收敛性质:s~u->v是图G某u,v的最短路径,而且在松弛边(u,v)之前的任何时间u.d=σ(s,u),则在操作过后总有v.d=σ(s,v)。
证明没有负环情况下Bellman-Ford算法能够找到最短路径。我们通过路径松弛性质来证明。考虑从源节点s到节点v,设p< v0 , v1 , v2 , ... , vk > 是从源节点s=v0到节点vk的任意一条最短路径,这里v0=s,vk=v ;因为最短路径都为简单路径,p最多包含|V|-1条边,因此k<=|V|-1。算法的内层循环每次循环所有的边。在第i次松弛操作时,这里i = 1 , 2 , 3 , ... , k,被松弛点边包含( vi-1 , vi )。根据路径松弛性质,v.d=vk.d=δ(s,vk)=δ(s,v)。
个人理解:要想寻找到一条最短路径p< v0 , v1 , v2 , ... , vk >,只需要从源节点开始,按照p路径的顺序进行松弛操作(也就是在任意的遍历路径L< v0 , va , vb , ... , vk >中,p是L的子序列),那么在每一次对P路径节点进行松弛操作的时候都是符合收敛性质的,这样就能确定下vk.d的值是最小值。而Bellman-Ford算法的每一次内层循环遍历的是所有的边,因此能保证至少一条边是满足p路径的顺序的,也就是每一次遍历所有边至少能将按照p路径的松弛顺序向外扩展一条边,而p路径最多有v-1条边,因此最多v-1次循环就能确保v.d=vk.d=δ(s,vk)=δ(s,v)。