【C++】【最短路径】Bellman-Ford 算法实现(贝尔曼-福特算法)(带负权边的图)【relaxation】


一、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实现过程:

  1. 循环次数为V-1次
  2. 循环遍历所有节点(V次),每次对节点的临边relaxation一次。(这一步实际上是对所有的边遍历了一次)(因为是遍历每个节点,然后遍历当前节点的临边)所以O(E)
  3. 结尾再对每个节点做一次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;
        }
    }
};

参考

模板类设计
bobo老师课程
bobo老师github

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值