zoukankan      html  css  js  c++  java
  • Bellman-Ford算法

    ------------------siwuxie095

       

       

       

       

       

       

       

       

    Bellman-Ford 算法

       

       

    这里介绍 Bellman-Ford 算法,和 Dijkstra 算法一样,

    它也是一个单源最短路径算法

       

       

    Bellman-Ford 算法解决了 Dijkstra 算法没有解决的问

    题:负权边问题,即 Bellman-Ford 算法中可以引入负

    权边

       

       

       

       

       

    看如下实例:

       

       

       

    顶点 1 到顶点 2 的负权边的存在,使得虽然当前从顶点 0

    顶点 1 的权值远远的高于从顶点 0 到顶点 2 的权值,但在绕

    道的过程中,负权边让大部分权值都抵消了,反而低于从顶点

    0 到顶点 2 的权值

       

       

       

    0 -> 1 -> 2 的路径比 0 -> 2 的路径更短,如下:

       

       

       

    不难看出,表面是在处理负权边,但本质上仍然是一次

    松弛操作

       

    换言之,虽然负权边使得 Dijkstra 算法失效了,但依然

    要依赖松弛操作

       

       

       

       

       

    Bellman-Ford 算法虽然解决了负权边问题,但它也有

    一定的局限性

       

    看如下实例:

       

       

       

    多出一条从顶点 2 到顶点 0 的负权边,就形成了一个负权环

    0 -> 1 -> 2 -> 0 这条路径的总权值为 -2

       

       

       

    当一个图中出现了负权环,那么从一点到任何一点只要能经

    过该负权环,权值就会更小,而想要找到所谓的最短路径,

    就一定要不停地在该负权环中转。因为每转一圈,得到的总

    权值就更小

       

       

    这样一来,相当于图中就不存在最短路径了,或 最短路径的

    结果是负无穷

       

       

    所以,在处理带有负权边的图时,如果图中拥有负权环,则

    该图就不再拥有最短路径

       

       

    「拥有负权环的图,没有最短路径」

       

       

    注意:不要认为负权环一定至少由三个顶点组成,事实上,

    两个顶点之间也可以形成负权环,如下图所示

       

       

       

       

    综上,Bellman-Ford 算法解决的就是图中可以有负权边,

    但不能有负权环的单源最短路径问题

       

    「前提:图中不能有负权环」

       

    不过 Bellman-Ford 算法比想象中更加出色,它不一定要

    遵守该前提。如果图中有负权环,Bellman-Ford 算法经

    过运行之后,虽然找不到最短路径,但是可以判断出图中

    有负权环

       

    「Bellman-Ford 算法可以判断图中是否有负权环」

       

    Bellman-Ford 算法如此神奇,相应的代价也是高昂的,

    它的时间复杂度:O(E*V)

       

       

       

       

       

    Bellman-Ford 算法的基本思想:

       

    如果一个图中没有负权环,从一点到另外一点的最短路径,

    最多经过所有 V 个顶点,有 V-1 条边,否则,存在顶点被

    经过了两次,即 存在负权环

       

       

    看如下实例:

       

       

       

    左边是一张连通带权有向图,右边是起始顶点 0 到各个顶点的

    当前最短距离的列表,起始顶点 0 到自身的距离是 0

       

       

    将顶点 0 进行标识,并作为当前顶点。对当前顶点 0 的所有相

    邻顶点依次进行一次松弛操作,同时更新列表

       

       

    然后将当前顶点 0 的所有相邻顶点依次当做新的当前顶点,并

    对新的当前顶点的所有相邻顶点依次进行一次松弛操作,同时

    更新列表

       

    … …

       

       

       

    对当前顶点进行一次松弛操作,就是找到了经过当前顶点的另

    外一条路径,多一条边,权值更小

       

       

    如果一个图中没有负权环,从一点到另外一点的最短路径,

    经过所有 V 个顶点,有 V-1 条边

       

       

    对所有顶点进行 V-1 次松弛操作,理论上就找到了从起始顶点

    到其它所有顶点的最短路径

       

       

    然后再尝试对所有顶点进行第 V 次松弛操作, 如果还可以继续

    松弛,就说明图中一定存在负权环

       

       

       

    注意:Bellman-Ford 算法主要针对有向图,因为如果是无向图,

    一旦图中存在负权边,就相当于存在负权环,而如果图中没有负

    权边,就可以直接使用 Dijkstra 算法,效率更高

       

       

       

       

       

    程序:

       

    Edge.h:

       

    #ifndef EDGE_H

    #define EDGE_H

       

    #include <iostream>

    #include <cassert>

    using namespace std;

       

       

    //边信息:两个顶点和权值

    template<typename Weight>

    class Edge

    {

       

    private:

       

    int a, b; //边的两个顶点ab(如果是有向图,就默认从顶点a指向顶点b

    Weight weight; //边上的权值

       

    public:

       

    Edge(int a, int b, Weight weight)

    {

    this->a = a;

    this->b = b;

    this->weight = weight;

    }

       

       

    //默认构造函数

    Edge(){}

       

       

    ~Edge(){}

       

       

    int v(){ return a; }

       

       

    int w(){ return b; }

       

       

    Weight wt() { return weight; }

       

       

    //知道边的一个顶点x,返回另一个顶点

    int other(int x)

    {

    assert(x == a || x == b);

    return x == a ? b : a;

    }

       

       

    //友元函数重载

    friend ostream &operator<<(ostream &os, const Edge &e)

    {

    os << e.a << "-" << e.b << ": " << e.weight;

    return os;

    }

       

       

    bool operator<(Edge<Weight> &e)

    {

    return weight < e.wt();

    }

       

       

    bool operator<=(Edge<Weight> &e)

    {

    return weight <= e.wt();

    }

       

       

    bool operator>(Edge<Weight> &e)

    {

    return weight > e.wt();

    }

       

       

    bool operator>=(Edge<Weight> &e)

    {

    return weight >= e.wt();

    }

       

       

    bool operator==(Edge<Weight> &e)

    {

    return weight == e.wt();

    }

    };

       

       

    #endif

       

       

       

    SparseGraph.h:

       

    #ifndef SPARSEGRAPH_H

    #define SPARSEGRAPH_H

       

    #include "Edge.h"

    #include <iostream>

    #include <vector>

    #include <cassert>

    using namespace std;

       

       

       

    // 稀疏图 - 邻接表

    template<typename Weight>

    class SparseGraph

    {

       

    private:

       

    int n, m; //n m 分别表示顶点数和边数

    bool directed; //directed表示是有向图还是无向图

    vector<vector<Edge<Weight> *>> g; //g[i]里存储的就是和顶点i相邻的所有边指针

       

    public:

       

    SparseGraph(int n, bool directed)

    {

    this->n = n;

    this->m = 0;

    this->directed = directed;

    //g[i]初始化为空的vector

    for (int i = 0; i < n; i++)

    {

    g.push_back(vector<Edge<Weight> *>());

    }

    }

       

       

    ~SparseGraph()

    {

       

    for (int i = 0; i < n; i++)

    {

    for (int j = 0; j < g[i].size(); j++)

    {

    delete g[i][j];

    }

    }

    }

       

       

    int V(){ return n; }

    int E(){ return m; }

       

       

    void addEdge(int v, int w, Weight weight)

    {

    assert(v >= 0 && v < n);

    assert(w >= 0 && w < n);

       

    g[v].push_back(new Edge<Weight>(v, w, weight));

    //1)顶点v不等于顶点w,即不是自环边

    //2)且不是有向图,即是无向图

    if (v != w && !directed)

    {

    g[w].push_back(new Edge<Weight>(w, v, weight));

    }

       

    m++;

    }

       

       

    //hasEdge()判断顶点v和顶点w之间是否有边

    //hasEdge()的时间复杂度:O(n)

    bool hasEdge(int v, int w)

    {

    assert(v >= 0 && v < n);

    assert(w >= 0 && w < n);

       

    for (int i = 0; i < g[v].size(); i++)

    {

    if (g[v][i]->other(v) == w)

    {

    return true;

    }

    }

       

    return false;

    }

       

       

    void show()

    {

       

    for (int i = 0; i < n; i++)

    {

    cout << "vertex " << i << ": ";

    for (int j = 0; j < g[i].size(); j++)

    {

    cout << "{to:" << g[i][j]->w() << ",wt:" << g[i][j]->wt() << "} ";

    }

    cout << endl;

    }

    }

       

       

       

    //邻边迭代器(相邻,即 adjacent

    //

    //使用迭代器可以隐藏迭代的过程,按照一定的

    //顺序访问一个容器中的所有元素

    class adjIterator

    {

    private:

       

    SparseGraph &G; //图的引用,即要迭代的图

    int v; //顶点v

    int index; //相邻顶点的索引

       

    public:

       

    adjIterator(SparseGraph &graph, int v) : G(graph)

    {

    this->v = v;

    this->index = 0;

    }

       

       

    //要迭代的第一个元素

    Edge<Weight> *begin()

    {

    //因为有可能多次调用begin()

    //所以显式的将index设置为0

    index = 0;

    //如果g[v]size()不为0

    if (G.g[v].size())

    {

    return G.g[v][index];

    }

       

    return NULL;

    }

       

       

    //要迭代的下一个元素

    Edge<Weight> *next()

    {

    index++;

    if (index < G.g[v].size())

    {

    return G.g[v][index];

    }

       

    return NULL;

    }

       

       

    //判断迭代是否终止

    bool end()

    {

    return index >= G.g[v].size();

    }

    };

    };

       

       

    #endif

       

       

       

    DenseGraph.h:

       

    #ifndef DENSEGRAPH_H

    #define DENSEGRAPH_H

       

    #include "Edge.h"

    #include <iostream>

    #include <vector>

    #include <cassert>

    using namespace std;

       

       

       

    // 稠密图 - 邻接矩阵

    template<typename Weight>

    class DenseGraph

    {

       

    private:

       

    int n, m; //n m 分别表示顶点数和边数

    bool directed; //directed表示是有向图还是无向图

    vector<vector<Edge<Weight> *>> g; //二维矩阵,存储边指针

       

    public:

       

    DenseGraph(int n, bool directed)

    {

    this->n = n;

    this->m = 0;

    this->directed = directed;

    //二维矩阵:nn列,全部初始化为NULL

    for (int i = 0; i < n; i++)

    {

    g.push_back(vector<Edge<Weight> *>(n, NULL));

    }

    }

       

       

    ~DenseGraph()

    {

    for (int i = 0; i < n; i++)

    {

    for (int j = 0; j < n; j++)

    {

    if (g[i][j] != NULL)

    {

    delete g[i][j];

    }

    }

    }

    }

       

       

    int V(){ return n; }

    int E(){ return m; }

       

       

    //在顶点v和顶点w之间建立一条边

    void addEdge(int v, int w, Weight weight)

    {

    assert(v >= 0 && v < n);

    assert(w >= 0 && w < n);

       

    //如果顶点v和顶点w之间已经存在一条边,就删掉,

    //之后按照传入权值重建一条边,即直接覆盖

    if (hasEdge(v, w))

    {

    delete g[v][w];

       

    //如果是无向图,还要删除和主对角线对称的值

    if (!directed)

    {

    delete g[w][v];

    }

       

    m--;

    }

       

    g[v][w] = new Edge<Weight>(v, w, weight);

       

    //如果是无向图,还要在和主对角线对称处添加值

    if (!directed)

    {

    g[w][v] = new Edge<Weight>(w, v, weight);

    }

       

    m++;

    }

       

       

    //hasEdge()判断顶点v和顶点w之间是否有边

    //hasEdge()的时间复杂度:O(1)

    bool hasEdge(int v, int w)

    {

    assert(v >= 0 && v < n);

    assert(w >= 0 && w < n);

    return g[v][w] != NULL;

    }

       

       

    void show()

    {

       

    for (int i = 0; i < n; i++)

    {

    for (int j = 0; j < n; j++)

    {

    if (g[i][j])

    {

    cout << g[i][j]->wt() << " ";

    }

    else

    {

    cout << "NULL ";

    }

    }

    cout << endl;

    }

    }

       

       

    //邻边迭代器(相邻,即 adjacent

    class adjIterator

    {

    private:

       

    DenseGraph &G; //图引用,即要迭代的图

    int v; //顶点v

    int index; //相邻顶点的索引

       

    public:

       

    adjIterator(DenseGraph &graph, int v) : G(graph)

    {

    this->v = v;

    this->index = -1;

    }

       

       

    //要迭代的第一个元素

    Edge<Weight> *begin()

    {

    //找第一个权值不为NULL的元素,即为要迭代的第一个元素

    index = -1;

    return next();

    }

       

       

    //要迭代的下一个元素

    Edge<Weight> *next()

    {

    for (index += 1; index < G.V(); index++)

    {

    if (G.g[v][index])

    {

    return index;

    }

    }

       

    return NULL;

    }

       

       

    //判断迭代是否终止

    bool end()

    {

    return index >= G.V();

    }

    };

    };

       

       

    #endif

       

       

       

    ReadGraph.h:

       

    #ifndef READGRAPH_H

    #define READGRAPH_H

       

    #include <iostream>

    #include <string>

    #include <fstream>

    #include <sstream>

    #include <cassert>

    using namespace std;

       

       

       

    //从文件中读取图的测试用例

    template <typename Graph, typename Weight>

    class ReadGraph

    {

       

    public:

    ReadGraph(Graph &graph, const string &filename)

    {

       

    ifstream file(filename);

    string line; //一行一行的读取

    int V, E;

       

    assert(file.is_open());

       

    //读取file中的第一行到line

    assert(getline(file, line));

    //将字符串line放在stringstream

    stringstream ss(line);

    //通过stringstream解析出整型变量:顶点数和边数

    ss >> V >> E;

       

    //确保文件里的顶点数和图的构造函数中传入的顶点数一致

    assert(V == graph.V());

       

    //读取file中的其它行

    for (int i = 0; i < E; i++)

    {

       

    assert(getline(file, line));

    stringstream ss(line);

       

    int a, b;

    Weight w;

    ss >> a >> b >> w;

    assert(a >= 0 && a < V);

    assert(b >= 0 && b < V);

    graph.addEdge(a, b, w);

    }

    }

    };

       

       

    #endif

       

       

       

    BellmanFord.h:

       

    #ifndef BELLMANFORD_H

    #define BELLMANFORD_H

       

    #include "Edge.h"

    #include <stack>

    #include <vector>

    using namespace std;

       

       

       

    //Bellman-Ford 算法实现最短路径

    template <typename Graph, typename Weight>

    class BellmanFord

    {

       

    private:

       

    Graph &G; //图的引用,即要进行操作的图

    int s; //起始顶点 ss source

    Weight* distTo; //起始顶点 s 到每个顶点的当前最短距离

    vector<Edge<Weight>*> from; //经由哪条边到达了当前顶点

    bool hasNegativeCycle; //该图是否有负权环

       

    bool detectNegativeCycle()

    {

    //对所有顶点再进行一次松弛操作(第 V 次)

    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->w()] || distTo[e->v()] + e->wt() < distTo[e->w()])

    {

    return true;

    }

    }

    }

       

    return false;

    }

       

       

    public:

       

    BellmanFord(Graph &graph, int s) :G(graph)

    {

       

    this->s = s;

    distTo = new Weight[G.V()];

    for (int i = 0; i < G.V(); i++)

    {

    from.push_back(NULL);

    }

       

    // Bellman-Ford

    //

    //对起始顶点 s 到自身的最短距离进行初始化,

    //由于不知道 distTo 数组中元素的具体类型,

    //所以使用模板类型Weight的默认构造函数,

    //如果指定的模板为 int,会被初始化为 0

    distTo[s] = Weight();

       

    //所有顶点都进行 V-1 次松弛操作

    for (int pass = 1; pass < G.V(); pass++)

    {

       

    //所有顶点

    for (int i = 0; i < G.V(); i++)

    {

    //注意:声明迭代器时,前面还要加 typename,表明

    //adjIterator Graph 中的类型,而不是成员变量

    typename Graph::adjIterator adj(G, i);

    //对当前顶点 i 的所有相邻顶点依次进行一次松弛操作

    for (Edge<Weight> *e = adj.begin(); !adj.end(); e = adj.next())

    {

    //1)如果还没有边到达相邻顶点 w

    //2)或:"经过"当前顶点 i 到相邻顶点 w 所得到的

    //路径小于"不经过"当前顶点 i 到相邻顶点 w 所得到

    //的路径,就进行一次松弛操作

    if (!from[e->w()] || distTo[e->v()] + e->wt() < distTo[e->w()])

    {

    distTo[e->w()] = distTo[e->v()] + e->wt();

    from[e->w()] = e;

    }

    }

    }

    }

       

    //对所有顶点进行第 V 次松弛操作,判断是否存在负权环

    hasNegativeCycle = detectNegativeCycle();

    }

       

       

    ~BellmanFord()

    {

       

    delete []distTo;

    }

       

       

    //判断图中是否存在负权环

    bool negativeCycle()

    {

    return hasNegativeCycle;

    }

       

       

    //顶点 s 到顶点 w 的最短距离

    Weight shortestPathTo(int w)

    {

    assert(w >= 0 && w < G.V());

    assert(!hasNegativeCycle);

    return distTo[w];

    }

       

       

    //判断顶点 s 到顶点 w 是否有路径

    bool hasPathTo(int w)

    {

    assert(w >= 0 && w < G.V());

    return from[w] != NULL;

    }

       

       

    //找到从顶点 s 到顶点 w 的最短路径的边的组成:通过from数组

    //从顶点 w 倒推回去,并存储在栈中,最后再从栈中转存到向量中

    void shortestPath(int w, vector<Edge<Weight>> &vec)

    {

       

    assert(w >= 0 && w < G.V());

    assert(!hasNegativeCycle);

       

    stack<Edge<Weight>*> s;

       

    Edge<Weight> *e = from[w];

       

    //直到倒推到起始顶点,对于有向图

    //来说,e->v() 即一条边的起点

    while (e->v() != this->s)

    {

    s.push(e);

    e = from[e->v()];

    }

    s.push(e);

       

    //只要栈不为空,就将栈顶元素放入

    //向量中,并出栈

    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);

       

    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;

    }

    }

    };

       

       

    #endif

       

       

       

    main.cpp:

       

    #include "SparseGraph.h"

    #include "DenseGraph.h"

    #include "ReadGraph.h"

    #include "BellmanFord.h"

    #include <iostream>

    using namespace std;

       

       

       

    int main()

    {

       

    string filename = "testG2.txt";

    int V = 5;

       

    //稀疏图

    SparseGraph<int> g = SparseGraph<int>(V, true);

    ReadGraph<SparseGraph<int>, int> readGraph(g, filename);

       

    cout << "Test Bellman-Ford:" << endl << endl;

    BellmanFord<SparseGraph<int>, int> bellmanFord(g, 0);

    if (bellmanFord.negativeCycle())

    {

    cout << "The graph contain negative cycle!" << endl;

    }

    else

    {

    for (int i = 1; i < V; i++)

    {

    cout << "Shortest Path to " << i << " : "

    << bellmanFord.shortestPathTo(i) << endl;

       

    bellmanFord.showPath(i);

       

    cout << "----------" << endl;

    }

    }

       

    system("pause");

    return 0;

    }

       

       

    //对所有顶点都要进行 V 次松弛操作,且对每一条边

    //都要遍历一遍,共 E 条边,所以最终 Bellman-Ford

    //算法的时间复杂度是 OV*E这个级别的

       

       

    运行一览:

       

       

       

       

    其中,testG2.txt 的内容如下:

       

       

       

    该文件可以分成两个部分:

       

    1)第一行:两个数字分别代表顶点数和边数

       

    2)其它行:每一行的前两个数字表示一条边,第三个数字表示权值

       

       

       

       

       

       

       

       

       

    【made by siwuxie095】

  • 相关阅读:
    win10 安装cmake报错: "Installation directory must be on a local drive"
    python量化笔记16之夏普比率计算公式
    python机器量化之十七:混淆矩阵(confusion_matrix)含义
    使用Aspose.word (Java) 填充word文档数据
    python笔记(十八)机器量化分析—数据采集、预处理与建模
    js 使FORM表单的所有元素不可编辑的示例代码 表
    Web前端面试题:写一个mul函数
    vue + electron 快速入门
    Java新特性stream流
    mycat中间件进行MySQL数据表的水平拆分
  • 原文地址:https://www.cnblogs.com/siwuxie095/p/7135598.html
Copyright © 2011-2022 走看看