假如你有一張地圖,地圖上給出了每一對相鄰城市的距離,從一個地點到另外一個地點,如何找到一條最短的路?最短路算法要解決的就是這類問題。定義:給定一個有(無)向圖,每一條邊有一個權值 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 和π 都更新一遍。過程本身很簡單,下邊是圖示:
源點 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://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 結構之法,參考效率分析部分