Dijstra算法---普通版、優先隊列優化版、堆優化版

假如你有一張地圖,地圖上給出了每一對相鄰城市的距離,從一個地點到另外一個地點,如何找到一條最短的路?最短路算法要解決的就是這類問題。定義:給定一個有(無)向圖,每一條邊有一個權值 w,給定一個起始點 S 和終止點 T ,求從 S 出發走到 T 的權值最小路徑,即爲最短路徑。最短路算法依賴一種性質:一條兩頂點間的最短路徑包含路徑上其他最短路徑。簡單的說就是:最短路徑的子路徑是最短路徑。這個用反證法很好證明。

一、鬆弛技術(Relaxation)

     瞭解最短路算法前,必須先了解鬆弛技術, 爲什麼叫鬆弛,有特定原因,有興趣可以去查查相關資料,如果簡化理解鬆弛技術,它本質上就是一個貪心操作。鬆弛操作:對每個頂點v∈V,都設置一個屬性d[v],用來描述從源點 s 到 v 的最短路徑上權值的上界,成爲最短路徑估計(Shortest-path Estimate),同時π[v]代表前趨。初始化僞代碼:

INITIALIZE-SINGLE-SOURCE(G, s)
for each vertex v ∈ V[G]
       do d[v] ← ∞   //距離
         π[v] ← NIL   //是否遍歷過
d[s] ← 0
鬆弛操作

初始化之後,對所有 v∈V,π[v] = NIL,對v∈V – {s},有 d[s] = 0 以及 d[v] = ∞。鬆弛一條邊(u, v),如果這條邊可以對最短路徑改進,則更新 d[v] 和π[v] 。一次鬆弛操作可以減小最短路徑估計的值 d[v] ,並更新 v 的前趨域π[v]。下面的僞代碼對邊(u,v)進行了一步鬆弛操作:

RELAX(u, v, w)
  if d[v] > d[u] + w(u, v)
     then d[v] ← d[u] + w(u, v)
          π[v] ← u

上邊的圖示中,左邊例子,最短路徑估計值減小,右邊例子,最短路徑估計值不變。當發現 v 到 u 有更近的路徑時,更新 d[v] 和π[v] 。

二、Dijkstra算法

解決最短路問題,最經典的算法是 Dijkstra算法,它是一種單源最短路算法,其核心思想是貪心算法(Greedy Algorithm),Dijkstra算法由荷蘭計算機科學家Dijkstra發現,這個算法至今差不多已有50年曆史,但是因爲它的穩定性和通俗性,到現在依然強健。另外,Dijkstra算法要求所有邊的權值非負。

Dijkstra算法思想爲:設 G = (V, E) 是一個帶權有向圖,把圖中頂點集合 V 分成兩組,第一組爲已求出最短路徑的頂點集合(用 S 表示,初始時 S 中只有一個源點,以後每求得一條最短路徑 , 就將其加入到集合 S 中,直到全部頂點都加入到 S 中,算法就結束了),第二組爲其餘未確定最短路徑的頂點集合(用 U 表示),按最短路徑長度的遞增次序依次把第二組的頂點加入 S 中。在加入的過程中,總保持從源點 v 到 S 中各頂點的最短路徑長度不大於從源點 v 到 U 中任何頂點的最短路徑長度。此外,每個頂點對應一個距離,S 中的頂點的距離就是從 v 到此頂點的最短路徑長度,U 中的頂點的距離,是從 v 到此頂點只包括 S 中的頂點爲中間頂點的當前最短路徑長度。(在實際的實現中只用一個布爾數組,true表示在S中,false表示在U中)僞代碼:

(維基百科版):

u := Extract_Min(Q) 在頂點集合 Q 中搜索有最小的 d[u] 值的頂點 u。這個頂點被從集合 Q 中刪除並返回給用戶。

 1  function Dijkstra(G, w, s)
 2     for each vertex v in V[G]                        // 初始化
 3           d[v] := infinity
 4           previous[v] := undefined
 5     d[s] := 0
 6     S := empty set
 7     Q := set of all vertices
 8     while Q is not an empty set                      // Dijkstra演算法主體
 9           u := Extract_Min(Q)
10           S := S union {u}
11           for each edge (u,v) outgoing from u
12                  if d[v] > d[u] + w(u,v)             // 拓展邊(u,v)
13                        d[v] := d[u] + w(u,v)
14                        previous[v] := u

如果我們只對在 s 和 t 之間尋找一條最短路徑的話,我們可以在第9行添加條件如果滿足 u = t 的話終止程序。現在我們可以通過迭代來回溯出 s 到 t 的最短路徑:

1 s := empty sequence 
2 u := t
3 while defined u
4       insert u to the beginning of S
5       u := previous[u]
現在序列 S 就是從 s 到 t 的最短路徑的頂點集. 



(算法導論版)

DIJKSTRA(G, w, s)

1  INITIALIZE-SINGLE-SOURCE(G, s)

2  S ← Ø

3  Q ← V[G]

4  while Q ≠ Ø

5      do u ←EXTRACT-MIN(Q)

6        S ← S ∪{u}

7        for each vertex v ∈ Adj[u]

8            do RELAX(u, v, w)

第 1 行將 d 和 π 初始化,第 2 行初始化集合 S 爲空集,4 ~ 8 行每次迭代,都從 U 中選取一個點加入到 S 中,然後所有的邊進行鬆弛操作,即每次迭代,整個圖的 d 和π 都更新一遍。過程本身很簡單,下邊是圖示:

Dijkstra算法過程

源點 s 是最左端頂點。最短路徑估計被標記在頂點內,陰影覆蓋的邊指出了前趨的值。黑色頂點在集合 S中,而白色頂點在最小優先隊列 Q = V – S 中。a) 第 4 ~ 8 行 while 循環第一次迭代前的情形。陰影覆蓋的頂點具有最小的 d 值,而且在第 5 行被選爲頂點 u 。b) ~ f) while 循環在第一次連續迭代後的情形。每個圖中陰影覆蓋的頂點被選作下一次迭代第 5 行的頂點 u。f) 圖中的 d 和π 值是最終結果。

Dijkstra算法時間主要消耗在尋找最小權值的邊,和鬆弛所有剩餘邊,所以 EXTRACT-MIN(Q) 這一步,更好的方法是使用優先隊列,優先隊列可以用二叉堆,斐波那契堆等來實現,下面的代碼,我用庫自帶的優先隊列,經這樣改造後,效率還是很可觀的。

理解最短路算法,最基礎,最簡單,最經典的要數這個題目:HDU 2544 最短路,純粹的算法練習題,用Dijkstra,我寫了三個代碼來實現。

1)鄰接矩陣 + Dijkstra,最簡單的方式,當然也是最好理解的方式:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <climits>

int map2544[105][105];
bool mark2544[105];
int dist2544[105];
int N2544,M2544;//頂點、邊

void init2544()
{
	for(int i=0;i<105;++i)
		for(int j=0;j<105;++j)
			map2544[i][j]=INT_MAX>>1;
}

void Dijkstra1(int src)//臨接矩陣
{
	for(int i=1;i<=N2544;++i)
	{
		dist2544[i]=map2544[src][i];
		mark2544[i]=false;
	}
	dist2544[src]=0;
	for(int i=1;i<=N2544;++i)
	{
		if(mark2544[i]) continue;
		int min=INT_MAX >> 1,k=src;
		for(int j=1;j<=N2544;++j)
		{
			if(!mark2544[j] && dist2544[j]<min)
			{
				min=dist2544[j],k=j;
			}
		}
		mark2544[k]=true;
		for(int i=1;i<=N2544;++i)
		{
			if(!mark2544[i] && dist2544[k]+map2544[k][i]<dist2544[i])
				dist2544[i]=dist2544[k]+map2544[k][i];
		}
	}
}

void Dijkstra2(int src)//臨接矩陣
{
	for(int i=1;i<=N2544;++i)
	{
		dist2544[i]=map2544[src][i];
		mark2544[i]=false;
	}
	dist2544[src]=0;
	for(;;)
	{
		int min=INT_MAX>>1,k=src;
		for(int j=1;j<=N2544;++j)
		{
			if(!mark2544[j] && dist2544[j]<min)
			{
				min=dist2544[j],k=j;
			}
		}
		if(min == INT_MAX>>1) break;
		mark2544[k]=true;
		for(int i=1;i<=N2544;++i)
		{
			if(!mark2544[i] && dist2544[k]+map2544[k][i]<dist2544[i])
				dist2544[i]=dist2544[k]+map2544[k][i];
		}
	}
}

int main()
{
	freopen("in.txt","r",stdin);
	while(scanf("%d%d",&N2544,&M2544))
	{
		if(N2544==0 && M2544==0) break;
		init2544();
		int A,B,C;
		for(int i=0;i<M2544;++i)
		{
			scanf("%d %d %d",&A,&B,&C);
			map2544[A][B]=map2544[B][A]=C;
		}
		Dijkstra2(1);
		printf("%d\n",dist2544[N2544]);
	}

	return 0;
}

2)鄰接表 + 優先隊列優化 +Dijkstra,效率更高,更實用的方式:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <climits>
#include <queue>
using namespace std;

#define MAXLEN 102          // 最大頂點數爲10
int N2544,M2544;//頂點、邊
int dist2544[MAXLEN];
bool mark2544[MAXLEN];

typedef struct node{                // 邊表結點
	node(){ next=NULL; }
	int adjvex;                    // 鄰接點域
	int info;						//數據域info
	struct node  *next;          // 指向下一個鄰接點的指針域
}EdgeNode;        
typedef struct vnode{			   // 頂點表結點
	vnode(){ firstedge=NULL; }
	int vertex;			   // 頂點域
	EdgeNode  *firstedge;	       // 邊表頭指針
}VertexNode;		
typedef VertexNode AdjList[MAXLEN]; // AdjList是鄰接表類型
typedef struct{  
	AdjList adjlist;              // 接表
	int n,e;                      // 頂點數和邊數
}ALGraph;					      // ALGraph是以鄰接表方式存儲的圖類型
ALGraph G;//此處發現如果在函數中傳遞G的指針式,HDU中一直WA,估計和傳遞參數有關,具體解釋請看《高質量C/C++指南》中7.4節

int top=0;
EdgeNode node2544[20005];
EdgeNode* GetNode2544()
{
	return node2544+top++;
}

bool CreateGraphAL ()
{
	int A,B,C;
	EdgeNode * s;
	scanf("%d%d",&(G.n),&(G.e));		// 讀入頂點數和邊數
	N2544=G.n,M2544=G.e;
	if(G.n==0 && G.e==0) return true;
	for (int k=0;k<G.e;k++)		// 建立邊表
	{ 
		scanf("%d %d %d",&A,&B,&C); // 讀入邊<Vi,Vj>的頂點對應序號
		//s=new EdgeNode;			// 生成新邊表結點s
		s=GetNode2544();
		s->adjvex=B;			// 鄰接點序號爲j
		s->info=C;			// 邊的權重
		s->next=G.adjlist[A].firstedge; // 將新邊表結點s插入到頂點Vi的邊表頭部
		G.adjlist[A].firstedge=s;

		//s=new EdgeNode;			// 生成新邊表結點s
		s=GetNode2544();
		s->adjvex=A;			// 鄰接點序號爲j
		s->info=C;			// 邊的權重
		s->next=G.adjlist[B].firstedge; // 將新邊表結點s插入到頂點Vi的邊表頭部
		G.adjlist[B].firstedge=s;
	}
	return false;
}

void init2544()
{
	for(int i=1;i<=G.n;++i)
		G.adjlist[i].firstedge=NULL;
	for(int i=0;i<20005;++i)
		node2544[i].next=NULL;
	top=0;
}

struct cmpnode {
	int v, dis;
	cmpnode () {}
	cmpnode (int V, int DIS) : v(V), dis(DIS) {}
	friend bool operator < (const cmpnode a, const cmpnode b) {
		return a.dis > b.dis;
	}
};

void Dijkstra3(int src)
{
	priority_queue<cmpnode> q;
	cmpnode first;

	for(int i=1;i<=N2544;++i)
	{
		dist2544[i]=INT_MAX>>1;
		mark2544[i]=false;
	}
	dist2544[src]=0;
	q.push(cmpnode(src,0));

	while(!q.empty())
	{
		first=q.top();q.pop();
		mark2544[first.v]=true;
		EdgeNode *s;
		s=G.adjlist[first.v].firstedge;
		while(s!=NULL)
		{
			if(!mark2544[s->adjvex] && first.dis+s->info < dist2544[s->adjvex])//第一個bool判斷能成立的原因,邊中沒有負值,所以根據貪心第一次遍歷到的時候距離最短
			{
				dist2544[s->adjvex]=first.dis+s->info;
				q.push(cmpnode(s->adjvex,dist2544[s->adjvex]));
			}
			s=s->next;
		}
	}
}
int main()
{
	freopen("in.txt","r",stdin);
	while(1)
	{
		init2544();
		if(CreateGraphAL()) break;
		Dijkstra3(1);
		printf("%d\n",dist2544[N2544]);
	}

	return 0;
}

如果對Dijkstra算法核心思想不是很理解,可能會問:Dijkstra算法爲什麼不能處理負權邊?

Dijkstra由於是貪心的,每次都找一個距源點最近的點(dmin),然後將該距離定爲這個點到源點的最短路徑(d[i] ← dmin);但如果存在負權邊,那就有可能先通過並不是距源點最近的一個次優點(dmin’),再通過這個負權邊 L (L < 0),使得路徑之和更小(dmin’ + L < dmin),則 dmin’ + L 成爲最短路徑,並不是dmin,這樣Dijkstra就被囧掉了。(總結一句話:由於次優點的距離已經大於最有優點,所以就算加上負值小於最優解也不對)比如n = 3,鄰接矩陣:
0, 3, 4

3, 0,-2

4,-2, 0

用Dijkstra求得 d[1,2] = 3,事實上 d[1,2] = 2,就是通過了 1-3-2 使得路徑減小。Dijkstra的貪心是建立在邊都是正邊的基礎上,這樣,每次往前推進,路徑長度都是變大的,如果出現負邊,那麼先前找到的最短路就不是真正的最短路,比如上邊的例子,這個算法也就算廢了。

另外,Dijkstra算法時間複雜度爲O(V2 +E)。源點可達的話,O(V * lgV + E * lgV) => O(E * lgV)。當是稀疏圖的情況時,此時 E = V2/lgV,所以算法的時間複雜度可爲 O(V2)。若是斐波那契堆作優先隊列的話,算法時間複雜度爲O(V * lgV + E)。


參考文獻:

http://blog.csdn.net/sixleaves/article/details/8865900  奇怪的構建臨街表的方法
http://www.cppblog.com/doer-xee/archive/2009/11/26/101972.aspx  參考普通的Dijkstra、優先隊列優化的Dijkstra、堆優化的Dijkstra的代碼
http://mindlee.net/2011/11/18/shortest-paths-algorithm/  參考各種最近距離的計算方法
http://blog.csdn.net/v_JULY_v/article/details/6096981 結構之法,參考效率分析部分

發佈了50 篇原創文章 · 獲贊 5 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章