1. 程式人生 > >圖演算法:2、計算帶有負權值的單源最短路徑:Bellman-Ford演算法

圖演算法:2、計算帶有負權值的單源最短路徑:Bellman-Ford演算法

原文地址:http://www.wutianqi.com/?p=1912

相關文章:

1.Dijkstra演算法:

2.Floyd演算法:

Dijkstra演算法是處理單源最短路徑的有效演算法,但它侷限於邊的權值非負的情況,若圖中出現權值為負的邊,Dijkstra演算法就會失效,求出的最短路徑就可能是錯的。這時候,就需要使用其他的演算法來求解最短路徑,Bellman-Ford演算法就是其中最常用的一個。該演算法由美國數學家理查德•貝爾曼(Richard Bellman, 動態規劃的提出者)和小萊斯特•福特(Lester Ford)發明。Bellman-Ford演算法的流程如下:
給定圖G(V, E)(其中V、E分別為圖G的頂點集與邊集),源點s,

  • 陣列Distant[i]記錄從源點s到頂點i的路徑長度,初始化陣列Distant[n]為, Distant[s]為0;
  •  
    以下操作迴圈執行至多n-1次,n為頂點數:
    對於每一條邊e(u, v),如果Distant[u] + w(u, v) < Distant[v],則另Distant[v] = Distant[u]+w(u, v)。w(u, v)為邊e(u,v)的權值;
    若上述操作沒有對Distant進行更新,說明最短路徑已經查詢完畢,或者部分點不可達,跳出迴圈。否則執行下次迴圈;
  • 為了檢測圖中是否存在負環路,即權值之和小於0的環路。對於每一條邊e(u, v),如果存在Distant[u] + w(u, v) < Distant[v]的邊,則圖中存在負環路,即是說改圖無法求出單源最短路徑。否則陣列Distant[n]中記錄的就是源點s到各頂點的最短路徑長度。

可知,Bellman-Ford演算法尋找單源最短路徑的時間複雜度為O(V*E).

首先介紹一下鬆弛計算。如下圖:


 

鬆弛計算之前,點B的值是8,但是點A的值加上邊上的權重2,得到5,比點B的值(8)小,所以,點B的值減小為5。這個過程的意義是,找到了一條通向B點更短的路線,且該路線是先經過點A,然後通過權重為2的邊,到達點B。
當然,如果出現一下情況


 

則不會修改點B的值,因為3+4>6。
 
Bellman-Ford演算法可以大致分為三個部分
第一,初始化所有點。每一個點儲存一個值,表示從原點到達這個點的距離,將原點的值設為0,其它的點的值設為無窮大(表示不可達)。
第二,進行迴圈,迴圈下標為從1到n-1(n等於圖中點的個數)。在迴圈內部,遍歷所有的邊,進行鬆弛計算。
第三,遍歷途中所有的邊(edge(u,v)),判斷是否存在這樣情況:
d(v) > d (u) + w(u,v)
則返回false,表示途中存在從源點可達的權為負的迴路。
 
之所以需要第三部分的原因,是因為,如果存在從源點可達的權為負的迴路。則 應為無法收斂而導致不能求出最短路徑。
考慮如下的圖:
 

經過第一次遍歷後,點B的值變為5,點C的值變為8,這時,注意權重為-10的邊,這條邊的存在,導致點A的值變為-2。(8+ -10=-2)
 
 

第二次遍歷後,點B的值變為3,點C變為6,點A變為-4。正是因為有一條負邊在迴路中,導致每次遍歷後,各個點的值不斷變小。
 
在回過來看一下bellman-ford演算法的第三部分,遍歷所有邊,檢查是否存在d(v) > d (u) + w(u,v)。因為第二部分迴圈的次數是定長的,所以如果存在無法收斂的情況,則肯定能夠在第三部分中檢查出來。比如
 

此時,點A的值為-2,點B的值為5,邊AB的權重為5,5 > -2 + 5. 檢查出來這條邊沒有收斂。
 
所以,Bellman-Ford演算法可以解決圖中有權為負數的邊的單源最短路徑問。

個人感覺演算法導論講解很不錯,把這一章貼出來和大家分享:

24.1 The Bellman-Ford algorithm

The Bellman-Ford algorithm solves the single-source shortest-paths problem in the general case in which edge weights may be negative. Given a weighted, directed graph G = (VE) with source s and weight function w : E → R, the Bellman-Ford algorithm returns a boolean value indicating whether or not there is a negative-weight cycle that is reachable from the source. If there is such a cycle, the algorithm indicates that no solution exists. If there is no such cycle, the algorithm produces the shortest paths and their weights.

The algorithm uses relaxation, progressively decreasing an estimate d[v] on the weight of a shortest path from the source s to each vertex v ∈ V until it achieves the actual shortest-path weight δ(sv). The algorithm returns TRUE if and only if the graph contains no negative-weight cycles that are reachable from the source.

BELLMAN-FORD(G, w, s)
1  INITIALIZE-SINGLE-SOURCE(G, s)
2  for i1 to |V[G]| - 1
3       do for each edge (u, v) ∈ E[G]
4              do RELAX(u, v, w)
5  for each edge (u, v) ∈ E[G]
6       do if d[v] > d[u] + w(u, v)
7             then return FALSE
8  return TRUE

Figure 24.4 shows the execution of the Bellman-Ford algorithm on a graph with 5 vertices. After initializing the d and π values of all vertices in line 1, the algorithm makes |V| – 1 passes over the edges of the graph. Each pass is one iteration of the for loop of lines 2-4 and consists of relaxing each edge of the graph once. Figures 24.4(b)-(e) show the state of the algorithm after each of the four passes over the edges. After making |V|- 1 passes, lines 5-8 check for a negative-weight cycle and return the appropriate boolean value. (We’ll see a little later why this check works.)

(單擊圖片可以放大)

Figure 24.4: The execution of the Bellman-Ford algorithm. The source is vertex s. The d values are shown within the vertices, and shaded edges indicate predecessor values: if edge (u, v) is shaded, then π[v] = u. In this particular example, each pass relaxes the edges in the order (t, x), (t, y), (t, z), (x, t), (y, x), (y, z), (z, x), (z, s), (s, t), (s, y). (a) The situation just before the first pass over the edges. (b)-(e) The situation after each successive pass over the edges. The d and π values in part (e) are the final values. The Bellman-Ford algorithm returns TRUE in this example.

The Bellman-Ford algorithm runs in time O(V E), since the initialization in line 1 takes Θ(V) time, each of the |V| – 1 passes over the edges in lines 2-4 takes Θ(E) time, and the for loop of lines 5-7 takes O(E) time.

#include <iostream>
#include <stdlib.h>
#include <string.h>

using namespace std;


/*
Bellman-Ford:計算帶有負權值的最短路徑,注意:帶有負環的最短路徑無法求得

演算法思想:
S1:初始化除源點外所有點的最短距離為無窮,源點距離為0
S2:執行|V-1|次迴圈,每次對所有邊進行鬆弛操作,即:如果d[u] + w(u,v) < d[v],則令d[v] = d[u] + w(u,v)
S3:遍歷每條邊,對每條邊檢測是否可以繼續鬆弛,即:如果d[u] + w(u,v) < d[v],說明存在負環,無法計算最短距離;若每條邊都不可以鬆弛,
則d[i]表明源點到節點i的最短距離
輸出:
如果存在負環,輸出:存在負環
如果不存在負環,按照節點下標從小到大的順序,輸出:源點到各個節點的最短距離

距離陣列的大小與頂點個數相同

輸入:
3(邊數) 3(頂點數) 0(源點)
0 1 5
1 2 3
2 0 -10
輸出:
存在負環

輸入:
10 5 0
0 1 6
0 2 7
1 2 8
1 3 5
1 4 -4
2 3 -3
2 4 9
3 1 -2
4 0 2
4 3 7
輸出:
2 7 4 -2
節點1到源點0的最短距離:2,路徑:0 2 3 1
節點2到源點0的最短距離:7,路徑:0 2
節點3到源點0的最短距離:4,路徑:0 2 3
節點4到源點0的最短距離:-2,路徑:0 2 3 4

*/

const int N = 10000;
int d[N];
int pre[N];
typedef struct Edge
{
	Edge(){}
	Edge(int iBeg , int iEnd , int iWeight):_iBeg(iBeg),_iEnd(iEnd),_iWeight(iWeight){} 
	void setEdge(int iBeg , int iEnd , int iWeight)
	{
		_iBeg = iBeg;
		_iEnd = iEnd;
		_iWeight = iWeight;
	}
	int _iBeg, _iEnd;
	int _iWeight;
}Edge;



bool bellmanFord(int iStart ,int iVertexNum , int iEdgeNum , Edge* pEdge)
{
	if(!pEdge)
	{
		return false;
	}
	//設定起始節點的前向節點為本身,用於列印單源最短路徑
	memset(pre, -1 , sizeof(pre));
	pre[iStart] = iStart;
	//初始化,對除源點外的頂點距離設定為無窮大
	for(int i = 0 ; i < iVertexNum ; i++ )
	{
		if(i != iStart)
		{
			d[i] = INT_MAX;
		}
		else
		{
			d[i] = 0;
		}
	}
	//迭代|V|-1次,每次對所有邊進行鬆弛
	for(int i = 0 ; i < iVertexNum - 1 ; i++)
	{
		//對每條邊遍歷,進行鬆弛處理
		for(int j = 0 ; j < iEdgeNum ; j++)
		{
			int u = pEdge[j]._iBeg;
			int v = pEdge[j]._iEnd;
			int iWeight = pEdge[j]._iWeight;
			//如果起點的距離值+邊的權重<終點的距離值,就進行鬆弛處理
			if(d[u] + iWeight < d[v])
			{
				d[v] = d[u] + iWeight;
				//記錄每次鬆弛後終點的前面的端點為
				pre[v] = u;
			}
		}
	}
	//負權檢測:遍歷所有邊,檢測是否還可以繼續鬆弛,如果是,表明存在負環,無法計算單源最短路徑
	for(int j = 0 ; j < iEdgeNum ; j++)
	{
		int u = pEdge[j]._iBeg;
		int v = pEdge[j]._iEnd;
		int iWeight = pEdge[j]._iWeight;
		//如果起點的距離值+邊的權重<終點的距離值,就進行鬆弛處理
		if(d[u] + iWeight < d[v])
		{
			return false;
		}
	}	
	return true;
}

//列印路徑,引數root:當前端點下標,採用先遞迴後列印的方式
void printPath(int root)
{
	//遞迴基
	if(pre[root] == root)
	{
		//列印起始節點
		cout << root << " ";
		return;
	}
	printPath(pre[root]);
	cout << root << " " ;
}

void process()
{
	int iVertexNum , iEdgeNum , iStart;
	Edge edgeArr[N];
	while(cin >> iEdgeNum >> iVertexNum >> iStart)
	{
		//生成邊
		for(int i = 0 ; i < iEdgeNum ; i++)
		{
			cin >> edgeArr[i]._iBeg >> edgeArr[i]._iEnd >> edgeArr[i]._iWeight;
		}
		//呼叫貝爾曼-福特演算法
		if(bellmanFord(iStart ,iVertexNum , iEdgeNum , edgeArr))
		{
			for(int i = 0 ; i < iVertexNum ; i++ )
			{
				if(i != iStart)
				{
					cout << "節點" << i << "到源點" << iStart << "的最短距離:" << d[i] << ",路徑:";
					printPath(i);
					cout << endl;
				}		
			}
			
		}
		else
		{
			cout << "存在負環" << endl;
		}
	}
}

int main(int argc,char* argv[])
{
	process();
	system("pause");
	return 0;
}



補充:
考慮:為什麼要迴圈V-1次?
答:因為最短路徑肯定是個簡單路徑,不可能包含迴路的,
如果包含迴路,且迴路的權值和為正的,那麼去掉這個迴路,可以得到更短的路徑
如果迴路的權值是負的,那麼肯定沒有解了

圖有n個點,又不能有迴路
所以最短路徑最多n-1邊

又因為每次迴圈,至少relax一邊
所以最多n-1次就行了