1. 程式人生 > >Bellman-Ford演算法及其優化

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