Bellman-Ford算法及其优化

与Dijkstra算法一样,我们定义一幅加权有向图的结构如下:

//带权有向图
struct EdgeWeightedDigraph
{
	size_t V; //顶点数
	size_t E; //边数
	map<int, forward_list<tuple<int, int, double>> adj; //改进后的邻接表,tuple存储的是边集
}

Bellman-Ford算法

在加权有向图的最短路径求解算法中,Dijkstra算法只能处理所有边的权值都是非负的图(是否有环不影响求解),而基于拓扑顺序的算法虽然能在线性时间内高效处理负权重图,但仅局限于无环图。为此还需要一个更为普遍的最短路径求解算法:能够处理负权重图,也能处理有环的情况。

Bellman-Ford算法是求含负权重图的单源最短路径的一种算法。其原理为连续进行松弛,对于含有V个顶点的加权有向图,在每次松弛时把每条边都更新一下,若在V-1次松弛后还能更新,则说明图中有负权重环,因此无法得出结果,否则就完成。

vector<int> Bellman-Ford(EdgeWeightedDigraph &g)
{
    vector<int> edge(g.V);

    //定义并初始化dis[]
    vector<double> dis(g.V, DBL_MAX);
    dis.at(0) = 0.0;

    //进行V-1次松弛
    for (size_t i = 0; i < g.V-1; ++i) //松弛计数
    {
        for (auto ite = g.adj.cbegin(); ite != g.adj.cend(); ++ite)
        {
            for (const auto &e : (*ite).second) //松弛操作
            {
                if (dis.at(get<0>(e)) + get<2>(e) < dis.at(get<1>(e)))
                {
                    dis.at(get<1>(e)) = dis.at(get<0>(e)) + get<2>(e);
                    edge.at(get<1>(e)) = get<0>(e);
                }
            }
        }
    }

    //判断是否存在负权重环
    for (auto ite = g.adj.cbegin(); ite != g.adj.cend(); ++ite)
    {
        for (const auto &e : (*ite).second)
        {
            if (dis.at(get<0>(e)) + get<2>(e) < dis.at(get<1>(e)))
            {
                cerr << "含有负权重环,无解\n";
                vector<int> tmp;
                return tmp;
            }
        }
    }

    return edge;
}

性能

朴素的Bellman-Ford算法实现非常简单,在每一轮迭代中都会放松E条边,共进行V轮迭代,因此时间复杂度为O(VE)。这种实现在实际应用中并不常见,因为它的效率不高,而且我们只需要对Bellman-Ford算法稍作修改就能大幅提高在一般场景下的运行时间。 

SPFA算法 

分析Bellman-Ford算法,最外层循环(迭代次数)V-1实际上是算法是否有解的上限,因为需要的迭代遍数等于最短路径树的高度。如果不存在负权重环,平均情况下的最短路径树的高度应该远远小于V-1,在此情况下,多余最短路径树高的迭代遍数就是时间上的浪费,由此,可以依次来实施优化。 

实际上,在任意一轮中许多边的松弛都不会成功:只有上一轮中的dis[]值发生变化的顶点指出的边才能够改变其他dis[]的值。即,从算法执行的角度来说,如果某一轮迭代中松弛操作未执行,说明此次迭代所有的边都没有被松弛,因此可以证明:至此后,边集中所有的边都不需要再被松弛,从而可以提前结束迭代过程。

为了实现这样的优化,我们可以用队列来记录松弛操作被成功执行的顶点。同时还需要一个向量mark[]来指示顶点是否已经存在于队列中,以防止将顶点重复插入队列。

vector<int> SPFA(EdgeWeightedDigraph &g)
{
    vector<int> edge(g.V);
    queue<int> q;
    vector<int> mark(g.V, 0);

    //定义并初始化dis[]
    vector<double> dis(g.V, DBL_MAX);
    dis.at(0) = 0.0;

    int v = (*g.adj.cbegin()).first;
    q.push(v);
    mark.at(v) = 1;
    int cnt = 0;

    while (!q.empty())
    {
        v = q.front();
        q.pop();
        mark.at(v) = 0;

        //松弛操作
        for (const auto &e : g.adj.at(v))
        {
            int w = get<1>(e);
            if (dis.at(v) + get<2>(e) < dis.at(w))
            {
                dis.at(w) = dis.at(v) + get<2>(e);
                edge.at(w) = v;
                if (mark.at(w) == 0)
                {
                    q.push(w);
                    mark.at(w) = 1;
                }
            }
            if (++cnt % g.V == 0)
            {
                cerr << "存在负权重环,无解\n";
                vector<int> tmp;
                return tmp;
            }
        }
    }
    return edge;
}

性能

SPFA算法是Bellman-Ford算法的改进,一般情况下其路径长度的比较次数的数量级为O(E+V) 。但如果加权有向图中存在负权重环,由于每次都会有边被松弛,因而不可能提前终止外层循环。这对应了最坏情况,其时间复杂度仍旧为O(VE) 。 

 

    原文作者:Bellman - ford算法
    原文地址: https://blog.csdn.net/qq_37653144/article/details/83537640
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞