1. 程式人生 > >Bellman-Ford演算法—求解帶負權邊的最短路徑

Bellman-Ford演算法—求解帶負權邊的最短路徑

1.Dijkstra不能得到含有負權邊圖(這裡就不是環路了)的單源最短路徑

Dijkstra由於是貪心的,每次都找一個距源點最近的點(dmin),然後將該距離定為這個點到源點的最短路徑(d[i]<--dmin);但如果存在負權邊,那就有可能先通過並不是距源點最近的一個次優點(dmin'),再通過一個負權邊L(L<0),使得路徑之和更小(dmin'+L<dmin),則dmin'+L成為最短路徑,並不是dmin,這樣Dijkstra就被囧掉了。


比如上圖:1—>2權值為5,1—>3權值為6,3—>2權值為-2,求1到2的最短路徑時,Dijkstra就會貪心的選擇權為5的1—>2,但實際上1—>3—>2才是最優的結果,這樣Dijkstra演算法就無法得到正確的結果。

實際上只要有權重為負權的迴路在,就無法得到真正的最短路徑,因為只要在負權迴路上不斷兜圈子,所得的最短路徑長度可以任意小。

雖然得不到最短路徑,但我們有時需要知道圖中是否存在負權迴路,而Bellman-Ford演算法就可以幫我們解決這個問題。

2、Bellman-Ford演算法

定義:如果從結點s到結點v的某條路徑上存在權重為負值的環路(必須是環路,不同於負權邊),我們定義詳見CLRS圖24-1.

Bellman-Ford演算法返回一個布林值,以表明是否存在一個從源節點可以到達的權重為負值的環路(這樣我們就知道這個圖是不存在最短路徑的了)。

如果存在這樣一個環路,演算法將告訴我們不存在解決方案;如果沒有這種環路存在,演算法將給出最短路徑和它們的權重。

程式碼如下:

#include <iostream>
using namespace std;
const int maxnum = 100;
const int maxint = 99999;

// 邊
typedef struct Edge{
	int u, v;    // 起點,重點
	int weight;  // 邊的權值
}Edge;

Edge edge[maxnum];     // 儲存邊的值
int  dist[maxnum];     // 結點到源點最小距離

int nodenum, edgenum, source;    // 結點數,邊數,源點

// 初始化圖
void init( )
{
	// 輸入結點數,邊數,源點
	cin >> nodenum >> edgenum >> source;
	for(int i=1; i<=nodenum; ++i)
		dist[i] = maxint;
	dist[source] = 0;
	for(int i=1; i<=edgenum; ++i)
	{
		cin >> edge[i].u >> edge[i].v >> edge[i].weight;
		if(edge[i].u == source)          //注意這裡設定初始情況
			dist[edge[i].v] = edge[i].weight;
	}
}

// 鬆弛計算
void relax(int u, int v, int weight)
{
	if(dist[v] > dist[u] + weight)
		dist[v] = dist[u] + weight;
}

bool Bellman_Ford()
{
	for(int i=1; i<=nodenum-1; ++i)
		for(int j=1; j<=edgenum; ++j)
			relax(edge[j].u, edge[j].v, edge[j].weight);
	bool flag = 1;
	// 判斷是否有負環路
	for(int i=1; i<=edgenum; ++i)
		if(dist[edge[i].v] > dist[edge[i].u] + edge[i].weight)
		{
			flag = 0;
			break;
		}
	return flag;
}
int main()
{
        init( );
	if(Bellman_Ford())                     //不存在負環路時才能輸出最短(從源點到每個頂點都有一個最短路徑)
	    for(int i = 1 ;i <= nodenum; i++)  //記住這種輸出方法!
			cout << dist[i] << endl;
	return 0;
}

演算法描述:
     1,.初始化:將除源點外的所有頂點的最短距離估計值 d[v] ←+∞, d[s] ←0;
  2.迭代求解:反覆對邊集E中的每條邊進行鬆弛操作,使得頂點集V中的每個頂點v的最短距離估計值逐步逼近其最短距離;(執行|v|-1次)
  3.檢驗負權迴路:判斷邊集E中的每一條邊的兩個端點是否收斂。如果存在未收斂的頂點,則演算法返回false,表明問題無解;否則演算法返回true,並且從源點可達的頂點v的最短距離儲存在 d[v]中。
 

對於Bellman-Ford演算法我們有兩個問題需要解決:

問題一:為什麼要迴圈|V| -1次,即為什麼演算法要對圖的每條邊都進行了|V| -1 次 鬆弛操作?

描述性證明:
首先指出,最短路徑肯定是個簡單路徑,不可能包含迴路。如果包含迴路,且迴路的權值和為正的,那麼去掉這個迴路,可以得到更短的路徑;如果迴路的權值是負的,那麼肯定沒有解了。
其次,從源點s可達的所有頂點如果存在最短路徑,則這些最短路徑構成一個以s為根的最短路徑樹。Bellman-Ford演算法的迭代鬆弛操作,實際上就是按頂點距離s的層次,逐層生成這棵最短路徑樹的過程。
在對每條邊進行1遍鬆弛的時候,生成了從s出發,層次至多為1的那些樹枝。也就是說,找到了與s至多有1條邊相聯的那些頂點的最短路徑;對每條邊進行第2遍鬆弛的時候,生成了第2層次的樹枝,就是說找到了經過2條邊相連的那些頂點的最短路徑……。圖有|V| 個點,又不能有迴路,所以最短路徑最多|V| -1邊(所有的頂點都在最短路徑上),因為最短路徑最多隻包含|v|-1 條邊,所以,只需要迴圈|v|-1 次。

其實說白了,如果我們從源點開始嚴格按照圖固有的層級順序依次鬆弛所有的邊,那邊一次迴圈下來就可以得到最短路徑,而無需迴圈|v|-1 次,但現在我們不能預先設定每條邊鬆弛的先後順序,即邊的鬆弛順序是隨機的,我們就只能去做最保險的事情——迴圈|v|-1 次,這樣,無論是以什麼樣的順序來鬆弛所有的邊,有一點可以保證就是每次迴圈至少會有一條邊被鬆弛了,就像上面所說的那樣,每次迴圈至少會把這棵最短路徑樹向外拓展一層(無論是以怎樣“惡劣”的邊的次序),那麼最遠的也就第|v|-1層(一般很少出現這種情況的,所以後面的迴圈會有大量的鬆弛操作是浪費的),迴圈|v|-1 次足以鬆弛到它。

看一幅圖就什麼都明白了:


設源點為 s ,

1)如果我們以如下次序來鬆弛所有的邊:( s,t )、( s,y )、( t,x )、( t,y )、( t,z )、( y,x )、( y,z )、( z,x )、( z,s )、( x,t ),即嚴格的層序順序,結果一次迴圈就得到了最短路徑;


2)如果我們以如下次序來鬆弛所有的邊:( z,x )、( z,s )、( x,t )、( y,x )、( y,z )、( t,x )、( t,y )、( t,z )、( s,t )、( s,y ),即最惡劣的次序來鬆弛所有的邊,這時就不是一次迴圈可以解決的了(但至少每次迴圈會使最短路徑樹從源點向外擴充套件一層)。

另外,每實施一次鬆弛操作,最短路徑樹上就會有一層頂點達到其最短距離,此後這層頂點的最短距離值就會一直保持不變,不再受後續鬆弛操作的影響。(但是,每次還要判斷鬆弛,這裡浪費了大量的時間,怎麼優化?單純的優化是否可行?)
如果沒有負權迴路,由於最短路徑樹的高度最多隻能是|v|-1,所以最多經過|v|-1遍鬆弛操作後,所有從s可達的頂點必將求出最短距離。如果 d[v]仍保持 +∞,則表明從s到v不可達
如果有負權迴路,那麼第 |v|-1 遍鬆弛操作仍然會成功,這時,負權迴路上的頂點不會收斂。

問題二:如何保證演算法的正確性,即存在負環路時,演算法返回false,不存在負環路時,演算法返回true?

分兩點證明,1)不存在負環路時,都有 v.d < = u.d + w ( u , v )——即 所有的邊都鬆弛到了,這一點我們在問題一已經論證。這時演算法返回true,而這顯然是成立的;2)存在負環路時,一定存在某條邊使得 v.d >u.d + w ( u , v ),此時falg=0,即false. 舉例如下


此時,點A的值為-2,點B的值為5,邊AB的權重為5,5 > -2 + 5. 檢查出來這條邊沒有收斂。

注1:一定要區分負權邊和負權環路!!!