單源點最短路徑算法:Bellman-Ford算法

背景知識

圖簡介

圖由節點和邊組成,邊有方向的圖稱爲有向圖,邊沒有方向的圖稱爲無向圖,最短路徑算法裏可以把無向圖視爲雙向連接的有向圖。
邊有權重的圖稱爲有權圖,邊沒有權重的圖稱爲無權圖,無權圖可以視爲邊的權重均爲1的圖。

單源點最短路徑

給定圖中的一個節點,求該節點到其他所有節點的最短路徑。

Bellman-Ford算法

概述

Bellman-Ford屬於DP算法,適用於含負權邊的圖,並且可以檢測圖中是否含有負權環(意味着沒有最短路徑),時間複雜度爲O(VE)

核心思想

對所有邊進行V-1次遍歷,第一次遍歷獲得邊數爲1的最短路徑,第二次遍歷獲得邊數爲2的最短路徑,以此類推,第V-1次遍歷獲得邊數爲V-1的最短路徑。V-1次遍歷後,進行第V次遍歷,如果能夠獲得更小的路徑長度,說明存在負環。

算法步驟

dis[]用於存儲每個節點到源點src的距離,preNode[]存儲每個節點的前驅節點
Step1:初始化dis[]爲INT_MAX,dis[src]初始化爲0. preNode[src]初始化爲-1.
Step2:遍歷圖中的每條邊edge(v,u),若dis[v]不爲INT_MAX,且dis[v] + weight(v,u) < dis[u],執行dis[u] = dis[v] + weight(v,u)。
Step3:重複Step2 V次,若某次遍歷沒有執行dis[u] = dis[v] + weight(v,u),即發生了鬆弛,結束Step3
Step4:判斷Step3最後一次執行是否發生鬆弛,若發生,說明圖中含有負權環,若沒有發生,說明圖中沒有負權環。

爲什麼能獲得最短路徑

可以從直觀上理解,第一次遍歷獲得了(距離源點)邊數量爲1的最短路徑,第二次在第一次的基礎上獲得了邊數量爲2的最短路徑,第三次在第二次的基礎上獲得了邊數量爲3的最短路徑,依次類推,有些節點到源點有很多條路徑,這些路徑的邊數可能不一樣也可能一樣,算法會在V-1循環中比較這些路徑的長度,取最小值作爲最短路徑(遍歷中邊數多的可能距離可能反而小於邊數少的,這個時候就會發生鬆弛了)。

爲什麼最後一次遍歷是否發生鬆弛可以判斷負環

有兩種情況會導致Step3結束,一是當次遍歷沒有發生鬆弛,二是已經循環了V次,情況一顯然不存在負環,不然肯定可以繼續鬆弛。情況二下,如果最後一次遍歷發生了鬆弛,說明第V次鬆弛了,意味着找到的邊長數爲V的最短路徑,而最長的最短路勁應該是V-1條邊, 所以肯定至少一條邊重複了,一定是出現環了,環路徑比非環更短,說明是負環。
舉個最簡單的例子:
這裏寫圖片描述
第一次遍歷 dis[1] = 2, dis[2] =INT_MAX
第二次遍歷 dis[1] = 2, dis[2] = 3;
已經遍歷了V-1即兩次了,第3次遍歷,dis[1] = dis[2] + wegiht(2,1) = 1,小於dis[1],所以會繼續發生鬆弛。
如果weight(1,2) = 3,環長度爲3-2=1,正環,這個時候第V次遍歷就不會發生鬆弛了。

C++實現

#include <iostream>
#include <vector>

using namespace std;

class Solution
{
public:
    vector<int> bellmanFord(int src, vector<vector<int>> &graph, vector<int> &preNode)
    {
        preNode = vector<int>(graph.size(), -1);
        ///transform to vector<pair<int,int>>
        vector<pair<int, int>> arcs;
        for(int i = 0; i < graph.size(); i++)
        {
            for(int j = 0; j < graph.size(); j++)
            {
                if(graph[i][j] != 0)
                {
                    arcs.push_back(pair<int, int>(i, j));
                }
            }
        }

        ///intialize distance array
        vector<int> distToSrc(graph.size(), INT_MAX);
        distToSrc[src] = 0;

        bool isSlacked = false;
        ///traverse and slack
        for(int i = 0; i < graph.size(); i++)
        {
            isSlacked = false;
            for(auto arc: arcs)
            {
                if(distToSrc[arc.first] != INT_MAX &&
                   distToSrc[arc.first] + graph[arc.first][arc.second] < distToSrc[arc.second])
                {
                    distToSrc[arc.second] = distToSrc[arc.first] + graph[arc.first][arc.second];
                    preNode[arc.second] = arc.first;
                    isSlacked = true;
                }
            }
            ///no slack in on traverse, break;
            if(!isSlacked)
            {
                break;
            }
        }
        /*
         *ifSlacked == true, means V's traverse is slacked which means 
         *have negative circle. otherwise isSlacked should be false
         */
        if(isSlacked)
        {
            return vector<int>();
        }
        return distToSrc;
    }

    int printPath()
    {

        vector<vector<int>> graph{{0, 4, 0, 0, 0, 0, 0, 8, 0},
                                  {4, 0, 8, 0, 0, 0, 0, 11, 0},
                                  {0, 8, 0, 7, 0, 4, 0, 0, 2},
                                  {0, 0, 7, 0, 9, 14, 0, 0, 0},
                                  {0, 0, 0, 9, 0, 10, 0, 0, 0},
                                  {0, 0, 4, 14, 10, 0, 2, 0, 0},
                                  {0, 0, 0, 0, 0, 2, 0, 1, 6},
                                  {8, 11, 0, 0, 0, 0, 1, 0, 7},
                                  {0, 0, 2, 0, 0, 0, 6, 7, 0}};

        vector<int> preNode;
        auto dists = bellmanFord(0, graph, preNode);

        ///print
        for(int i = 0; i < dists.size(); i++)
        {
            cout << i << " " << dists[i] << ": ";
            int node = i;
            stack<int> s;
            while(node != -1)
            {
                s.push(node);
                node = preNode[node];
            }
            while(!s.empty())
            {
                cout << s.top() << " ";
                s.pop();
            }
            cout << endl;
        }
    }

int main()
{
    Solution().printPath();
    return 0;
}

The End

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章