一、Bellman-Ford 算法 是什么?
求解单源最短路径问题的一种算法。
它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。
其优于Dijkstra 算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达O(VE)。
1.特点
Bellman-Ford 支持有负权边的存在。同时能检测出图中有没有负权环
算法时间复杂度:O(EV) 其中E为边数、V为节点数。
算法的核心其实是relaxation,松弛操作,这对于有权图来说,十分重要。
因为两个节点中的最短路径,在有权途中,绕一圈反而比直接连接更优,带了负权边,更是如此。
其核心思想就是:对每个节点(V次)遍历一次所有的边(E次),找到源点到每个点的最短路径。
2.思想
遍历一个节点时,对其做V-1次松弛操作(因为每个其他的节点都有可能使得绕行的权值更低)
V-1次松弛操作:即对下图中节点0,对节点1做第二次松弛的时候发现:0-1-2路径更短,就更新0-2的最短路径为1 。(同时,还要对节点0再多做2次松弛,一共做V-1 = 4次松弛操作。)
其中v-1次松弛,其实是遍历了所有的边O(E)。
分析:
上面看到:对节点0做第二次松弛,能找到从0经过两条边的最短路径。
即: 对一个节点多做一次松弛操作,就是找到这个点的另一条路径,多一条边,权值更小
从一个点到另一个点的最短路径,最多是经过了所有的节点,包含了V-1条边。所以我们要对每个点进行V-1次松弛。
复杂度分析:
V-1次relaxation,其实就是遍历了所有的的边O(E),
一共需要做节点V次,所以时间复杂度为0(EV)。
二、简易实现
bobo老师的实现代码解读::
//代码涉及:(文末参考中由相关简介)
模板类、模板函数
最小索引堆
1.解读
算法初始化过程:
distTo 为存储从source到各个节点的最短路径。初始化为节点个数个空数组。用模板类型的默认构造来初始化。
from 为了记录最短路径中,当前节点是从哪个边过来的。可以恢复整条路经出来。
让from[s]不为NULL, 表示初始s节点可达且距离为0
this->s = s;
distTo = new Weight[G.V()];
// 初始化所有的节点s都不可达, 由from数组来表示
for( int i = 0 ; i < G.V() ; i ++ )
from.push_back(NULL);
// 设置distTo[s] = 0, 并且让from[s]不为NULL, 表示初始s节点可达且距离为0
distTo[s] = Weight();
from[s] = new Edge<Weight>(s, s, Weight());
Bellman-Ford实现过程:
- 循环次数为V-1次
- 循环遍历所有节点(V次),每次对节点的临边relaxation一次。(这一步实际上是对所有的边遍历了一次)(因为是遍历每个节点,然后遍历当前节点的临边)所以O(E)
- 结尾再对每个节点做一次relaxation,判断是否有负权环。
实际上就是对每个节点都做V-1次的松弛操作,只是V-1次作为了外层循环。内部是遍历每个节点而已。
同时,因为是按照节点的索引遍历节点,from[e->v()]就是为了判断当前节点是否已经遍历了,如果遍历了,才看与他临边的另一个节点需不需要relaxation。否则当前这个点还没有遍历过,遍历他的临节点毫无意义。
一个节点做了V-1次relaxation,就能找到最短路径。同时,如果还能进行第V次松弛,就说明 图中有负权环。这就是最后一步的意义。
// Bellman-Ford的过程
// 进行V-1次循环, 每一次循环求出从起点到其余所有点, 最多使用pass步可到达的最短距离
for( int pass = 1 ; pass < G.V() ; pass ++ ){
// 每次循环中对所有的边进行一遍松弛操作
// 遍历所有边的方式是先遍历所有的顶点, 然后遍历和所有顶点相邻的所有边
for( int i = 0 ; i < G.V() ; i ++ ){
// 使用我们实现的邻边迭代器遍历和所有顶点相邻的所有边
typename Graph::adjIterator adj(G,i);
for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() )
// 对于每一个边首先判断e->v()可达
// 之后看如果e->w()以前没有到达过, 显然我们可以更新distTo[e->w()]
// 或者e->w()以前虽然到达过, 但是通过这个e我们可以获得一个更短的距离, 即可以进行一次松弛操作, 我们也可以更新distTo[e->w()]
if( from[e->v()] && (!from[e->w()] || distTo[e->v()] + e->wt() < distTo[e->w()]) ){
distTo[e->w()] = distTo[e->v()] + e->wt();
from[e->w()] = e;
}
}
}
hasNegativeCycle = detectNegativeCycle();
// 判断图中是否有负权环
bool detectNegativeCycle(){
for( int i = 0 ; i < G.V() ; i ++ ){
typename Graph::adjIterator adj(G,i);
for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() )
if( from[e->v()] && distTo[e->v()] + e->wt() < distTo[e->w()] )
return true;
}
return false;
}
2.完整代码
来自bobo老师github,文末有连接。
// 使用BellmanFord算法求最短路径
template <typename Graph, typename Weight>
class BellmanFord{
private:
Graph &G; // 图的引用
int s; // 起始点
Weight* distTo; // distTo[i]存储从起始点s到i的最短路径长度
vector<Edge<Weight>*> from; // from[i]记录最短路径中, 到达i点的边是哪一条
// 可以用来恢复整个最短路径
bool hasNegativeCycle; // 标记图中是否有负权环
// 判断图中是否有负权环
bool detectNegativeCycle(){
for( int i = 0 ; i < G.V() ; i ++ ){
typename Graph::adjIterator adj(G,i);
for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() )
if( from[e->v()] && distTo[e->v()] + e->wt() < distTo[e->w()] )
return true;
}
return false;
}
public:
// 构造函数, 使用BellmanFord算法求最短路径
BellmanFord(Graph &graph, int s):G(graph){
this->s = s;
distTo = new Weight[G.V()];
// 初始化所有的节点s都不可达, 由from数组来表示
for( int i = 0 ; i < G.V() ; i ++ )
from.push_back(NULL);
// 设置distTo[s] = 0, 并且让from[s]不为NULL, 表示初始s节点可达且距离为0
distTo[s] = Weight();
from[s] = new Edge<Weight>(s, s, Weight()); // 这里我们from[s]的内容是new出来的, 注意要在析构函数里delete掉
// Bellman-Ford的过程
// 进行V-1次循环, 每一次循环求出从起点到其余所有点, 最多使用pass步可到达的最短距离
for( int pass = 1 ; pass < G.V() ; pass ++ ){
// 每次循环中对所有的边进行一遍松弛操作
// 遍历所有边的方式是先遍历所有的顶点, 然后遍历和所有顶点相邻的所有边
for( int i = 0 ; i < G.V() ; i ++ ){
// 使用我们实现的邻边迭代器遍历和所有顶点相邻的所有边
typename Graph::adjIterator adj(G,i);
for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() )
// 对于每一个边首先判断e->v()可达
// 之后看如果e->w()以前没有到达过, 显然我们可以更新distTo[e->w()]
// 或者e->w()以前虽然到达过, 但是通过这个e我们可以获得一个更短的距离, 即可以进行一次松弛操作, 我们也可以更新distTo[e->w()]
if( from[e->v()] && (!from[e->w()] || distTo[e->v()] + e->wt() < distTo[e->w()]) ){
distTo[e->w()] = distTo[e->v()] + e->wt();
from[e->w()] = e;
}
}
}
hasNegativeCycle = detectNegativeCycle();
}
// 析构函数
~BellmanFord(){
delete[] distTo;
delete from[s];
}
// 返回图中是否有负权环
bool negativeCycle(){
return hasNegativeCycle;
}
// 返回从s点到w点的最短路径长度
Weight shortestPathTo( int w ){
assert( w >= 0 && w < G.V() );
assert( !hasNegativeCycle );
assert( hasPathTo(w) );
return distTo[w];
}
// 判断从s点到w点是否联通
bool hasPathTo( int w ){
assert( w >= 0 && w < G.V() );
return from[w] != NULL;
}
// 寻找从s到w的最短路径, 将整个路径经过的边存放在vec中
void shortestPath( int w, vector<Edge<Weight>> &vec ){
assert( w >= 0 && w < G.V() );
assert( !hasNegativeCycle );
assert( hasPathTo(w) );
// 通过from数组逆向查找到从s到w的路径, 存放到栈中
stack<Edge<Weight>*> s;
Edge<Weight> *e = from[w];
while( e->v() != this->s ){
s.push(e);
e = from[e->v()];
}
s.push(e);
// 从栈中依次取出元素, 获得顺序的从s到w的路径
while( !s.empty() ){
e = s.top();
vec.push_back( *e );
s.pop();
}
}
// 打印出从s点到w点的路径
void showPath(int w){
assert( w >= 0 && w < G.V() );
assert( !hasNegativeCycle );
assert( hasPathTo(w) );
vector<Edge<Weight>> vec;
shortestPath(w, vec);
for( int i = 0 ; i < vec.size() ; i ++ ){
cout<<vec[i].v()<<" -> ";
if( i == vec.size()-1 )
cout<<vec[i].w()<<endl;
}
}
};