Bellman-ford(解決負權邊)

Dijkstra 算法雖然好,但是他不能解決帶有負權邊的(邊的權值爲負數)的圖,下面我們就來說一下幾乎完妹求最短路徑的算法Bellman-ford。Bellman-ford算法也非常簡單,核心代碼只有幾行,並且可以完美的解決帶有負權的圖,先來看看這個核心代碼吧

for(int k = 1 ; k <= n - 1 ; k ++)
{
    for(int i = 1 ; i < m ; i ++)
    {
        if(dis[v[i]] > dis[u[i]] + w[i])
            dis[v[i]] = dis[u[i]] + w[i] ;
    }
}

上邊的代碼外循環共循環了n - 1 次(n爲頂點的個數),內循環共循環了m次(m代表邊的個數)即枚舉每一條邊, dis 數組是的作用和dijkstra 算法一樣,也是用來記錄源點到其餘各個頂點的最短路徑,u,v,w 三個數組用來記錄邊的信息。例如第i條邊存儲在u[i]、v[i]、w[i]中,表示從頂點u[i]到頂點v[i]這條邊(u[i] --> v[i])權值爲w[i]。

兩層for循環的意思是:看看能否通過u[i]--->v[i] (權值爲w[i])這條邊,使的1號頂點的距離變短。即1號頂點到u[i]號頂點的距離(dis[u[i]]) 加上 u[i] ---> v[i]這條邊(權值爲w[i])的值是否比原來1號頂點到v[i]號距離(dis[v[i]])要小。這點感覺和迪傑斯特拉的鬆弛操作有點兒像。。。

我們把每一條邊都鬆弛一遍後會怎麼樣呢?我們來舉個例子,求下圖1號頂點到其餘所有頂點的最短路徑。



我們還是用一個dis數組來存儲1號頂點到所有頂點的距離。

我們先來初始化:


上方右圖中每個頂點旁的值爲該定點的最短路的“估計值”(當前1號頂點到他的距離),即dis中對應的值。根據邊給出的順序,先來處理一下第一條邊“2-3-2”(2--2-->3通過這條邊進行鬆弛)即判斷dis[3]是否大於dis[2]+2。此時的dis[3]是∞,dis[2]的值也是∞,因此dis[2] + 2也是∞,所以這條邊鬆弛失敗。

同時我們繼續處理第二條邊“1--2 --       -3” (1--“-3”-->2 ) 我們發現dis[2] > dis[1] + (-3) ,通過這條邊可以使dis[2]的值從∞變爲 -3 ,所以這個點鬆弛成功。我們可以用同樣的方法來處理剩下的一條邊,對所有的邊進行一遍鬆弛操作後的結果如下。



我們能發現,在對每一條邊都進行一次鬆弛操作後,已經使dis[2]和dis[5]的值變小,即1號頂點到2號頂點和1號頂點到5號頂點的距離都變短了。

接下來我們要對所有邊再進行一輪鬆弛操作,操作過程大致和上邊的一樣,再看看會有什麼變化。


      在這一-輪鬆弛時,我們發現,現在通過“2 3 2”(2→3)這條邊,可以使1號頂點到3號頂點的距離(dis[3]) 變短了。愛思考的同學就會問了,這條邊在上一一輪也鬆弛過啊,爲什麼上一一輪鬆弛失敗了,這一"輪卻成功了呢?因爲在第一 輪鬆弛過後,1號頂點到2號頂點的距離(dis[2]) 已經發生了變化,這一-輪再通過“232”(2-→3)這條邊進行鬆弛的時候,已經可以使1號頂點到3號頂點的距離(dis[3]) 的值變小。
    換句話說,第1輪在對所有的邊進行鬆弛之後,得到的是從1號頂點“只能經過一條邊”到達其餘各頂點的最短路徑長度。第2輪在對所有的邊進行鬆弛之後,得到的是從1號頂點“最多經過兩條邊”到達其餘各頂點的最短路徑長度。如果進行k輪的話,得到的就是1號頂點“最多經過k條邊”到達其餘各頂點的最短路徑長度。現在又有一一個新問題:需要進行多少輪呢?

    只要進行n - 1 輪就可以了,因爲在一個含有n個頂點的圖中,任意兩點之間的最短路徑最多包含n-1條邊。
   我們這個算法真的就只能包含n-1條邊嗎?最短路徑中不能包含迴路嗎?
   答案是:不可能!最短路徑肯定是一個不包含迴路的簡單路徑。迴路分爲正權迴路(即迴路權值之和爲正)和負權迴路(即迴路權值之和爲負)。我們分別來討論一下爲什麼這兩種迴路都不可能有。如果最短路徑中包含正權迴路,那麼去掉這個迴路,一定可以得到更短的路徑。如果最短路徑中包含負權迴路,那麼肯定沒有最短路徑,因爲每多走一次 負權迴路就可以得到更短的路徑。  因此,最短路徑肯定是-一個不包含迴路的簡單路徑,即最多包含n-1條邊,所以進行n-1輪鬆弛就可以了。
  扯了半天,回到之前的例子,繼續進行第3輪和第4輪鬆弛操作,這裏只需進行4輪就可以了,因爲這個圖一共只有5個頂點。




這裏看似貌似不需要第四輪,因爲執行完第四輪沒有任何變化!沒錯,其實就是最多進行  n - 1 輪鬆弛。

整個算法用一句話概括就是:對所有的邊進行n-1次鬆弛操作。核心代碼就只有幾行,如下:

for(int k = 1 ; k <= n - 1 ; k ++) //進行n-1輪鬆弛
{
    for(int i = 1 ; i < m ; i ++)  // 枚舉每一條邊
    {
        if(dis[v[i]] > dis[u[i]] + w[i])  //嘗試對每一條邊鬆弛
            dis[v[i]] = dis[u[i]] + w[i] ;
    }
}

   我們來總結一下。因爲最短路徑上最多有n-1條邊,所以Bellman-Ford算法最多有n-1個階段。在每-一個階段,我們對每一條邊 都要執行鬆弛操作。其實每實施一次鬆弛操作, 就會有一些頂點已經求得其最短路,即這些頂點的最短路的“估計值”變爲“確定值”。此後這些頂點的最短路的值就會一直保持不變,不再受後續鬆弛操作的影響(但是,每次還是會判斷是否需要鬆弛,這裏浪費了時間,是否可以優化呢? )。在前k個階段結束後,就已經找出了從源點發出“最多經過k條邊”到達各個頂點的最短路。直到進行完n-1個階段後,便得出了最多經過n-1條邊的最短路。
  Bellman-Ford算法的完整的代碼如下。
#include<bits/stdc++.h>
const int INF = 99999999;
using namespace std;
int main()
{
    int u[100] , v[100] , w[100] , dis[100] , n , m ;
    cin>>n>>m;
    for(int i = 1 ; i <= m ; i ++)
    {
        cin>>u[i] >> v[i] >> w[i];
    }
    for(int i = 1 ; i  <= n ; i ++)
    dis[i] = INF;
    dis[1] = 0;
    for(int k = 1 ; k <= n - 1 ; k ++)
        for(int i = 1 ; i <= m ; i ++)
            if(dis[v[i]] > dis[u[i]] + w[i])
                dis[v[i]] = dis[u[i]] + w[i];
            for(int i = 1 ; i <= n ; i ++)
                cout<<dis[i]<<" ";
    return 0 ;
}


/*
5 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3
*/
除此之外,bellman-ford 算法還可以檢測出一個圖是否含有負權迴路。如果進行n-1輪鬆弛操作之後仍然存在

if(dis[v[i]] > dis[u[i]] + w[i])

                dis[v[i]] = dis[u[i]] + w[i];

的情況,也就是說在進行n-1輪鬆弛後,仍可以繼續成功鬆弛,那麼此圖必然存在負權迴路。如果一個圖沒有負權迴路,那麼最短路徑所包含的邊最多爲n-1條,即進行n-1輪鬆弛操作後最短路不會再發生變化。如果在n-1輪鬆弛後最短路仍然可以 發生變化,則這個圖一定有負權迴路,管不見代碼如下:

 for(int k = 1 ; k <= n - 1 ; k ++)
        for(int i = 1 ; i <= m ; i ++)
            if(dis[v[i]] > dis[u[i]] + w[i])
                dis[v[i]] = dis[u[i]] + w[i];
                //檢測負權迴路
                flag = 0 ;
                for(int i = 1 ; i <= m ; i ++)
                if(dis[v[i]] > dis[u[i]] + w[i])
                    flag = 1 ;
                if(flag == 1)
                    printf("此圖沒有負權迴路\n");
   顯,Bellman-Ford 算法的時間複雜度是O(NM),這個時間複雜度貌似比Dijkstra 算法還要高,我們還可以對其進行優化。在實際操作中,Bellman-Ford算法經常會在未達到n-1輪鬆弛前就已經計算出最短路,之前我們已經說過,n-1其實是最大值。因此可以添加一個一維數組用來備份數組dis。如果在新-一輪的鬆弛中數組dis沒有發生變化,則可以提前跳出循環,代碼如下。
#include<bits/stdc++.h>
const int INF = 9999999;
using namespace std;
int main()
{
    int u[100] , v[100] , w[100] , dis[100] , n , m , ck , flag;
    cin>>n>>m;
    for(int i = 1 ; i <= m ; i ++)
    {
        cin>>u[i] >> v[i] >> w[i];
    }
    for(int i = 1 ; i  <= n ; i ++)
    dis[i] = INF;
    dis[1] = 0;
    for(int k = 1 ; k <= n - 1 ; k ++)
    {
        ck = 0 ; //用來標記本輪鬆弛操作中數組dis是否會發生更新
        for(int i = 1 ; i <= m ; i ++)
        {
            if(dis[v[i]] > dis[u[i]] + w[i])
            {
                 dis[v[i]] = dis[u[i]] + w[i];
                 ck = 1 ;  //數組dis發生更新,改變check的值
            }
        }
        if(ck == 0)
            break;   //如果dis數組沒有更新,提前退出循環結束算法
    }
    flag = 0 ;
    for(int i = 1 ; i <= m ; i ++)
    if(dis[v[i]] > dis[u[i]] + w[i])
                flag = 1;
                if(flag == 1)
                    printf("此圖包含有負權迴路\n");
                else
                {
                    for(int i = 1 ; i <= n ; i ++)
                        printf("%d ",dis[i]);
                }
    return 0 ;
}

/*
5 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3
*/
     Bellman-Ford算法的另外一種優化在文中已經有所提示:在每實施一次鬆弛操作後,就會有一些項點已經求得其最短路,此後這些頂點的最短路的估計值就會一直保持不變, 不再受後續鬆弛操作的影響,但是每次還要判斷是否需要鬆弛,這裏浪費了時間。這就啓發我們:每次僅對最短路估計值發生變化了的頂點的所有出邊執行鬆弛操作。
      美國應用數學家Richard Bellman (理查德。貝爾曼)於1958 年發表了該算法。此外Lester Ford, Jr在1956年也發表了該算法。因此這個算法叫做Bellman-Ford算法。其實EdwardF. Moore在1957年也發表了同樣的算法,所以這個算法也稱爲Bellman-Ford-Moore算法。Edward F. Moore很熟悉對不對?就是那個在“如何從迷宮中尋找出路”問題中提出了廣度優先搜索算法的那個傢伙。

例題:
POJ1860、POJ2240、POJ3259



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