Dijkstra最短路徑算法分析

   Dijkstra算法屬於稍微複雜一些的算法,理解起來也不是那麼容易,當初在網上搜最短路徑算法的時候感覺大多數博文都講的不是那麼通俗,主要是代碼,許多需要註釋的地方都省略了。也不是說這樣做不好,註釋的越少越能提高程序員閱讀代碼的能力。但是對新人來講,先給一個註釋詳細的代碼,然後接下來自己多動手敲幾遍(不要看之前的代碼),這樣可能更有效果。要想理解算法不自己完整地敲一遍是不可能的。

算法分析:對於一個有向圖G=<V,E>,和源點(計算的是源點到其他的距離)如下圖:

現在我們選F點作爲源點,箭頭上的數值即爲該邊的權值(算法的前提:沒有負權值),沒有直接通路的兩點間的距離記爲MAX。

  我們先思考一個問題,所有F點到其他點的最短的直接距離是多少(直接距離:無中間點的一條有向邊的距離,如F→A,而F→B→A不是)。想必大家都知道,如上圖,F點到其他點的直接距離最短的明顯是F→B,顯然假如不存在負權的邊的話,F→B即爲從F到B的最短路徑。現在,我們把源點切換到B,再做一次剛纔的查找,我們可以得出B到C的最短路徑爲B→C,而非B→A→D→C。現在就是算法的關鍵,我們來思考一下F到C的最短路徑是多少?

  F到C的最短路徑其實就是F→B→C,那麼爲什麼是這樣呢?我們需要用一下貪心的思想,每一次都要做出來最優的選擇,這樣總結果才可能是最優的。F→B,B→C均是按照最最短的直接距離選擇的,那麼F到C的最短路就是F→B→C,那麼真的是這樣嗎?

  可能很多人會疑問,假如F→C的距離爲6的話,豈不是比F→B→C短的多?說的非常對,F→B→C僅僅是恰好爲最短路徑,纔會有這樣的結果。所以我們需要提出一個當前最短路徑的概念。當前最短路徑即爲截止到當前計算爲止的最短路,這時候我們想一想剛纔的疑惑,爲什麼會出現F→C的距離爲6時,比我們F→B→C的距離要短呢。因爲F→B→C僅僅是截止到當前計算爲止的最短路徑,而我們的計算是沿着一條線的,每次比較也僅僅侷限於從當前點到其他點直接距離路徑的比較,自然可能忽略一些可能更短的路,所以我們在每次更新當前最短路時需要加入源點到N點的直接路徑與源點到N點的當前最短路徑比較(當前點到N點的直接距離最短)。

  這樣的話,可能又有人有疑問了,爲什麼只比較源點到N點的距離?這就是Dijkstra算法的巧妙之處,我們思考如下的一個情景(與上圖無關):B→C = 7,B→A = 10,A→C = 10,顯然B到C的最短路爲B→C而非B→A→C,有人可能會說,像剛纔一樣,假如B→A = 2,A→C = 3的話不就又犯了和剛纔一樣的錯誤了嗎。那我們想想,假如B→A < B→C的話那麼最短直接距離的選擇的時候還會不會選擇B→C了呢?顯然,假如我們選擇了B→C作爲最短直接距離則B→A一定比B→C大,這樣的話無論接下來A→C的距離多短總距離都不會超過B→C,現在爲什麼不能有負權的邊的原因大家也會清楚了(假如有負權的話要用Floyd算法)。

  這點我們搞清楚的話,接下來的步驟就是不斷的循環,我會放上我寫的一個帶有詳盡註釋的算法實現,附有一個圖示的測試樣例,測試過完全可以運行,大家可以複製到IDE中自行測試,記住一定要自己多敲幾遍。

算法實現

 

#define MAX 10000
#define NUM 6

//最終目的:求出源點到其他點的距離 
//origin:選定的源點 
//dist[a][b]:由 a 點到 b 點的距離,當 a == b 時,dist[a][b] == 0 
void dijkstra(int origin, int dist[][NUM]){ //這裏爲了方便用了int類型的權值,可以自行更改爲其他類型 
	int miniDist[NUM] = {0}; //當前最短距離,在所有循環結束時爲最終的最短距離 
	int p[NUM]; //p : previovs  (當前最短路的)前驅頂點 		ATTENTION:從1開始記 
	int s[NUM]; //s[i] == 1 表示源點到i點的最短路徑已經求出 ATTENTION:不是"當前"最短距離 
	int min = 0; //當前最短距離
	int i,j; //循環變量
	int k = 0; //k第一次出現時有解釋 
	int pre = 0;//前驅頂點,與p[n]關聯
	int tempOrigin = origin; //用一個臨時變量來進行運算避免因爲之後的改動丟失原始參數
	
	for(i = 0; i < NUM; i++) {
		miniDist[i] = dist[origin][i];
		if (miniDist[i] != MAX) {
			p[i] = origin + 1; //將所有與源點有聯通的點的前驅頂點記爲 源點+1
			//+1的原因是需要將無通路的頂點的前驅頂點記爲0,如果不+1則要將無通路的前驅頂點記爲-1 
		} else {
			p[i] = 0;
		}
		s[i] = 0; //初始化s數組 
	}
	s[origin] = 1;
	
	//以下循環爲算法主要部分 
	for(i = 0; i < NUM - 1; i++) {
		min = MAX + 1; //這樣可以將無通路的點也加入計算
		for(j = 0; j < NUM; j++) {
			//兩種情況會更新當前最短路:
			//1:(非當前)最短路未求出 
			//2:源點到該點的距離是(當前)最短的 
			if( !s[j] && (miniDist[j] < min) ) {
				min = miniDist[j];
				k = j;
			}
		}
		s[k] = j; //如上,源點到k是最短路
		for(j = 0; j < NUM; j++) {
			//兩種情況會更新最短路
			//1:(非當前)最短路未求出
			//2: (當前)間接最短路小於(當前)直接最短路
			//"miniDist[k] + dist[k][j]" 表示從源點到j點的(當前)間接最短路
			//"miniDist[j]" 表示從源點到j點的(當前)直接最短路 
			if( !s[j] && (miniDist[j] > miniDist[k] + dist[k][j]) ){ 
				miniDist[j] = miniDist[k] + dist[k][j];
				p[j] = k + 1; //更新前驅頂點 
			}
		}
	}
	//循環結束,所有的當前最短路即爲所求最短路 
	
	//打印最短路和前驅頂點
	//ATTENTION:這時的前驅頂點從0開始計數 
	for(i = 0; i < NUM; i++) {
		printf("最短路:%6d\t路徑:%d",miniDist[i],i);
		pre = p[i];
		while( (pre != 0) && (pre != origin + 1) ){
			printf("<-%d",pre - 1);
			pre = p[pre - 1];
		}
		printf("<-%d\n",origin);
	}
}

int main(){
	//以下爲樣例,可以自行試驗
	int dist[6][6];
	for(int i = 0; i < 6; ++i){
		for(int j = 0; j < 6; ++j){
			if(i == j){
				dist[i][j] = 0;
				continue;
			}
			dist[i][j] = 10000;
		}
	}
	dist[5][1] = 5;
	dist[0][1] = 6;
	dist[1][0] = 18;
	dist[1][5] = 10;
	dist[5][3] = 25;
	dist[5][0] = 24;
	dist[1][2] = 7;
	dist[4][2] = 4;
	dist[3][2] = 12;
	dist[2][3] = 15;
	dist[2][0] = 9;
	dist[0][3] = 8; 
	dijkstra(5,dist);
}

 

 

 

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