最短路徑演算法:克魯斯卡爾演算法和迪傑斯特拉演算法(天勤資料結構高分筆記)
迪傑斯特拉演算法演算法思想:
設有兩個頂點集合S和T,集合S存放途中已經找到最短路徑的頂點,集合T存放的是途中剩餘頂點。初始狀態是,集合S只包含源點V0,然後不斷從集合T中
選取到頂點V0的路徑長度最短的頂點Vu併入到初始集合中。集合S每併入一個新的頂點Vu,都要修改頂點V0到集合T中頂點最短路徑長度值。不斷重複此過
程,直到集合T中所有頂點全部併入到S中為止。
在理解“集合S沒併入一個新的頂點Vu,都要修改頂點V0到集合T中頂點的最短路徑長度值”時候需要注意:在Vu被選入S中之後,Vu被確定為路徑上的頂點,
此時Vu就像到達T中頂點的中轉站,多了一箇中轉站,就會多一些到達T中頂點的新的路徑,而這些新的路徑有可能迴避之前V0到T中頂點的路徑要短,因此
需要修改原有V0達到其他頂點的路徑長度。此時對於T中的一個頂點Vk,有兩種情況:一種是V0不經Vu到達Vk的路徑長度為a(舊的路徑長度),另一種是V0經
過Vu到達Vk的路徑長度為b(新的路徑長度)。如果a<=b,則什麼也不用做;如果a>=b,則用b來代替a。用同樣的方法處理T中其他頂點。當T中所有頂點都被處
理完時,會出現一組新的V0到T中各個頂點的路徑,這些路徑中有一條最短的,對應了T中一個頂點,就是新的Vu,將其併入S。重複上述過程,最後T中所有
的頂點都會被併入S中,此時就可以得到V0到圖中所有頂點的最短路徑
迪傑斯特拉演算法的執行過程:
需要引進3個輔助陣列:dist[]、path[]、set[]。
dist[Vi]表示當前已經找到的從V0到每個終端Vu的最短路徑長度。它的初始狀態為:若從V0到Vi有邊,則dist[Vi]為邊上的權值,否則dist[Vi]為∞。
path[Vi]中儲存從V0到Vi最短路徑上Vi的一個頂點,假設最短路徑上的頂點序列為V0,V1,V2.......Vi-1,Vi,則path[Vi] = Vi-1。path[]的初始狀態為:如果
V0到Vi有邊,則path[Vi] = V0,否則path[Vi] = -1。
set[]標記為陣列,set[Vi] == 0表示Vi在T中,即沒有被併入最短路徑;set[Vi] == 1表示Vi在S中,即已經被併入最短路徑。set[]的初態為:set[V0] = 1,其
餘元素全為0
迪傑斯特拉演算法執行過程如下:
1、從當前dist[]陣列中選出最小值,假設為dist[Vu],將set[Vi]設定為1,表示當前新併入的頂點為Vu;
2、迴圈掃描圖中頂點,對每個頂點進行以下檢測:
假設當前頂點為Vj,檢索Vj是否已經被併入S中,即檢視set[Vj] == 1是否成立。如果set[Vj] == 1,則什麼都不做;如果set[Vj] == 0,則比較dist[Vj]
和dist[Vu]+w的大小,其中w為邊<Vu,Vj>的權值。這個比較就是要看V0經過舊的最短路徑到達Vj和V0經過含有Vu的新的路徑達到Vj哪個更短,如果dist[Vj]
> dist[Vu]+w,則用新的路徑來更新舊的,並把頂點Vu加入到路徑中,且作為路徑上Vj之前的那個頂點,否則什麼都不做
3、對1和2迴圈執行n-1次(n圖中頂點個數)即可得到V0到其餘所有頂點的最短路徑。
path[]陣列中其實儲存的是一顆樹。這是一顆用雙親儲存結構表示的樹,通過這棵樹可以打印出從源結點到任何一個頂點最短路徑經過的所有頂點。樹的雙親表示法只
能輸出由葉子結點到根結點路徑上的結點,而無法實現逆向輸出。因此需要藉助一個棧來實現逆向輸出。列印路徑的函式如下:
void Printf_Path(int path[],int a) { stack<int> pathstack; //下面這個迴圈負責由葉子結點到根結點的順序將其逆向輸出 while (path[a] != -1) { pathstack.push(path[a]); a = path[a]; } pathstack.push(a); while (!(pathstack.empty() )) { cout << pathstack.top() << endl; //出棧並打印出棧元素 pathstack.pop(); } } //根據上面的講解可以寫出迪傑斯特拉演算法 void DijsTra(MGraph &G, int v, int dist[], int path[]) { int set[MAXSIZE] = { 0 }; int min, i, j, u; //從這句開始對個數組進行初始化 for (i = 0; i < G.n; ++i) { dist[i] = G.edges[v][i]; set[i] = 0; if (G.edges[v][i] < INT8_MAX) path[i] = v; else path[i] = -1; } set[v] = 1; path[v] = -1; //將當前其實頂點v納入集合S中,並將路徑設定為-1,表示這就是所有路徑的起始頂點 //初始化結束 //關鍵操作開始 for (i = 0; i < G.n; ++i) { min = INT8_MAX; //這個迴圈每次從剩餘頂點中選取出一個頂點,通往這個頂點的路徑在通往所有剩餘頂點的路徑中長度是最短的 for (j = 0; j < G.n; ++j) { if (0 == set[j] && dist[j] < min) { u = j; min = dist[j]; } } set[u] = 1; //將選出的頂點放入最短路徑中 //下面這個迴圈以剛併入的頂點作為中間點,對所有通往剩餘頂點的路徑進行檢測 for ( j = 0 ;j < G.n ; ++j) { //下面這個if語句判斷頂點u的加入是否會出現通往頂點j更短的路徑,如果出現則改變原來的路徑及其長度,否則什麼也不做 if (0 == set[j] && dist[u] + G.edges[u][j] < dist[j]) { dist[j] = dist[u] + G.edges[u][j]; path[j] = u; } } } //關鍵操作結束 }//函式結束的時候,dist[]陣列中存放了頂點v到其餘頂點的最短路徑長度,path[]中存放了頂點v到其餘各頂點的最短路徑
迪傑斯特拉演算法主要部分為一個雙重迴圈,外層迴圈為兩個並列的單層迴圈,可以任取一個迴圈內的操作為基本操作。基本操作執行的總 次數即為雙重迴圈執行的操作次數,為n*n次。因此迪傑斯特拉演算法的時間複雜度為O(n*n)
弗洛伊德演算法:迪傑斯特拉演算法是求圖中某一頂點到其餘各頂點的最短路徑。如果求圖中任意一對頂點之間的最短路徑,。則通常使用的是弗洛伊德演算法
弗洛伊德演算法採用的演算法思想就是動態規劃。動態規劃,百度百科上的解釋說:“動態規劃”通過把原來較複雜的問題遞迴的分解
為一組較簡單的子問題,並通過儲存每個子問題的解,使得每個子問題只計算一次就可以解決原問題的思想。
動態規劃的本質就是遞迴或者迭代。遞迴是從大變小,例如斐波那契數列中求F(n),則F(n)=F(n-1)+F(n-2),n從大到小變為1,這就是遞迴
迭代過程則和遞迴過程相反,迭代是由小變大的。例如斐波那契數列中求F(n),則F(i+2)=F(i+1)+F(i),i從1開始逐漸增大變為n。
動態規劃就是在迭代的基礎上增加一個數組(假設為A[n]),將每一步的F(i)的計算結果儲存在陣列中A[i]。這在在迭代過程中可能多次會用到F(i)
,(例如求F(i+1) = F(i) + F(i-1) 和求F(i+2)=F(i+1)+F(i)時都要兩次用到F(i) )此時就不需要再進行大量的計算了,直接從陣列A[i]中取就行了
動態規劃的目的就是為了減少迭代和遞迴過程中不必要的重複性計算,每個中間結果計算一次就行了
弗洛伊德演算法求解最短路徑的一般過程:
1、設定兩個矩陣A[][]和Path[][]。初始時將圖的鄰接矩陣賦值給A[][].。將矩陣Path[][]中元素全部設定為-1。
(根據Path矩陣可以算出任意兩個頂點間最短路徑的序列,它儲存就是的兩頂點間的最短路徑上的後繼頂點的資訊)
2)以頂點k為中間結點,k取0~n-1 (n為圖中頂點個數),對圖中所有頂點對{i,j}進行如下檢測:
如果A[i][j] > A[i][k] + A[k][j]的值,就將Path[i][j]改為k,否則什麼都不做
程式碼如下:
void Floyd(MGraph G, int path[][MAXSIZE])
{
int i = 0,j = 0,k = 0; //區域性變數最好初始化為0,否則容易讀到記憶體中的髒資料
int A[MAXSIZE][MAXSIZE] = { 0 };
for ( i = 0 ; i < G.n ; ++i)
{
for (j = 0; j < G.n; ++j)
{
A[i][j] = G.edges[i][j];
path[i][j] = -1;
}
}
//下面這個三層迴圈是弗洛伊德演算法的主要操作,完成了以k為中間點對所有頂點對(i,j)進行檢測和修改
for ( k = 0; k < G.n ; ++k)
for (i = 0; i < G.n; ++i)
for (j = 0; j < G.n; ++j)
{
if(A[i][j] > A[i][k]+ A[k][j])
path[i][j] = k;
}
}
//弗洛伊德演算法的時間複雜度為O(n*n*n)