[最短路徑問題]Dijkstra演算法(含還原具體路徑)

Amαdeus發表於2022-11-27

前言

在本篇文章中,我將介紹 Dijkstra 演算法解決 單源最短路徑問題 ,同時還包含了具體路徑的還原。以下是我自己的全部學習過程與思考,參考書籍為 《資料結構》(C++語言版) 鄧俊輝 編著

(本文作者: Amαdeus,未經允許不得轉載哦。)



最短路徑問題

最短路徑概述

在當今這個繁華的時代,我們時時刻刻生活在一張龐大的城市網路中,我們也許會想著從溫暖的家鄉奔向自己未來奮鬥的都市,抑或是夢想著逃離城市的喧囂去往那片心中的靜謐之地......然而我們始終離不開一個問題————我們如何更快地、更短距離地前往我們所規劃的目的地呢? 在這個時候,人們通常會規劃好到達目的地的最佳路線,這其實就是最短路徑問題在實際生活中的一個簡單應用。?

最短路徑問題 : 給定一個帶權有向圖 G = (V, E, W),同時給定一個源點 u (u ∈ V),我們要找出從源點 u 出發到其它各點的最短路徑距離,並得出這些最短路徑的具體路徑有哪些邊構成。

其實我們要求的就是從 源點 u 出發到 其它各點 的最短路徑所組成的路線網路,也就是一個 最短路徑樹。?

最短路徑示例

我們以下面這個帶權有向圖為示例

我們若以 A 為源點,得到如下的最短路徑

我們可以把源點到各點最短路徑用綠色標記一下

我們可以看出所有的最短路徑構成了一個最短路徑樹

我們要求的從 源點 到 其它各點 的最短路徑所組成的路線網路,就是這個最短路徑樹。

最短路徑樹性質

單調性

在上面的圖中,我們不難發現,當我們確定了源點 u 到某個其它的點 v 的最短路徑時,在這個最短路徑的具體路線中,若有一箇中轉點 t,那麼在這個最短路徑中從源點 ut 的路徑也一定是 ut最短路徑(之一)。也就是說,假設源點 uv 的最短路徑為 p,那麼p任意的字首路徑 q 一定是最優的(最短路徑之一)。如果 q 不是最優的,那麼就會存在另一個更短的路徑比 p 更短。

這個性質還是很重要的,是解決單源最短路徑問題的核心

我們畫個圖來理解一下

歧義性

在上面的闡述中也稍微提到一點,就是最短路徑其實不一定是唯一的,有可能存在兩個路徑,它們的路徑距離一樣且都是最短的,那麼此時我們二選其一就可以啦。還有一個問題就是,我們的邊權都應當是正數,如果邊權存在非正數,那麼我們是無法定義這個圖中的最短路徑的(距離確實不能是非正數呀,除了自己到自己?)。

無環性

這個性質其實很好理解,既然我們得到的所有最短路徑構成的是一個 最短路徑樹,那麼作為一個樹,它必不會存在環。也可以由之前的 單調性 得出這個性質。



Dijkstra演算法

演算法簡介

Dijkstra 演算法是由荷蘭電腦科學家 Edsger Wybe Dijkstra 在1956年提出的,一般解決的是 帶權有向圖單源最短路徑問題
接下來介紹如何用 Dijkstra 演算法求解 單源最短路徑問題。?

演算法思路

Dijkstra 演算法將會充分利用 最短路徑樹單調性 這一性質。先定下源點 u,然後採用 貪心 的策略,不斷去訪問與源點 u 相接且之前未被訪問過的最近的頂點 v(這句話裡相接的意思是指可以從 u 到達 v),使得當前的最短路徑樹得到擴充,一直到所有頂點都在當前的最短路徑樹中,那麼就得到了源點 u 到其他所有頂點 v 的最短路徑。

我們將當前最短路徑樹所有的頂點所構成的集合稱為 集合S,而不在當前最短路徑樹中的頂點所構成的集合稱為集合V-S

演算法步驟

1、首先需要定義一個輔助陣列 flag[],用於標記每個頂點是否處於當前的 最短路徑樹 中,後續我們將 最短路徑樹 稱為 集合S。在初始情況下,我們會先將源點 u 劃入 集合S;

2、然後我們需要再定義一個陣列 dist[],用於記錄當前從源點 uv (v∈V-S)的最短路徑距離,比如dist[vi]就表示 uvi 的當前最短路徑距離。

集合S每一次擴充都需要選擇當前不在集合S中且到源點 u 最短距離的頂點 t 作為擴充點,並且將其劃入集合S。之後的擴充操作中,就以這個 t 作為中轉點對 dist[v] 進行更新,使其記錄的距離減小。在不斷擴充集合S的過程中,dist[v]的記錄的距離大小不斷減小(可能不變),直到最後,其記錄的便是整個圖中uv* 的最短的距離;

另外,一開始我們要先初始化源點 u 到其鄰接的頂點的距離。

3、為了還原具體路徑,我們還需要一個輔助陣列 pre[],用於記錄最短路徑中每個頂點的前驅頂點。比如 pre[v],其記錄的是 uv 的最短路徑中,頂點 v 的前驅頂點。在不斷擴充集合S的過程中,如果可以藉助當前的擴充點 t 到達 v 的距離更短,我們也要更新 v 的前驅為 t,即 pre[v] = t

同樣的,我們也要初始化源點 u 為其每個鄰接頂點的前驅。

動態演示

(1)

(2)

(3)

(4)

(5)

(6)

(7)



程式實現

以下程式是基於 圖的鄰接矩陣 實現的,如果不瞭解的話,可以先去康康別的大佬有關鄰接矩陣構建圖的文章哦 ❤❤❤

Dijkstra核心程式碼

//距離記錄陣列 , 前驅陣列
int dist[MAX], pre[MAX];  
//集合S標記陣列。如果flag[i]=true,說明該頂點i已經加入到集合S(最短路徑集合);否則i屬於集合V-S
bool flag[MAX];            

void Dijkstra(Graph *G, int u){
    for(int v = 0; v < G->nodenums; v++){
	dist[v] = G->edge[u][v];  //初始化源點u到各鄰接點v的距離
	flag[v] = false;
	if(dist[v] != INF)
	    pre[v] = u;           //若有鄰接邊,頂點v有前驅頂點u
	else
	    pre[v] = -1;          //若沒有,先初始化為-1
    }
    flag[u] = true;               //初始化集合S,只有一個元素: 源點u
    dist[u] = 0;                  //初始化源點u到自己的最短路徑為0
    
    /*   在集合V-S中尋找距離源點u最近的頂點t,使當前最短路徑樹最優  */
    for(int i = 0; i < G->nodenums; i++){
    	int tmp = INF, t = u;
    	for(int v = 0; v < G->nodenums; v++){
    	    if(!flag[v] && dist[v] < tmp){
    		//不在集合S中 並且 更小距離
    		t = v;            //記錄在V-S中距離源點u最近的頂點v
    		tmp = dist[v];
    	    }
    	}

    	if(t == u)
    	    return;               //未找到直接終止
    	flag[t] = true;           //否則, 將t加入集合S
        
        /*   更新集合V-S中與t鄰接的頂點到u的距離,擴充套件當前最短路徑樹  */
    	for(int v = 0; v < G->nodenums; v++){
    	    if(!flag[v] && G->edge[t][v] != INF){
    		//不在集合S中 且 有邊
                if(dist[v] > dist[t] + G->edge[t][v]){
                    //源點u可以藉助t到達v的距離更短
                    dist[v] = dist[t] + G->edge[t][v];
                    pre[v] = t;
                }
    	    }
    	}
    }
}


還原具體路徑程式碼

我使用了 C++ 自帶的 棧 stack,來實現最短路徑具體路徑的還原。因為記錄的是每個頂點的前驅,所以恰好可以利用 棧 stack 的先進後出的性質。

//還原源點u到各點具體路徑
void ShowShortParth(Graph G, int u){
    for(int v = 0; v < G.nodenums; v++){
	if(dist[v] == INF || dist[v] == 0)
	    continue;
	cout<<"\n點"<<G.apex[u]<<" 到 點"<<G.apex[v]<<" 的最短路徑距離為: "<<dist[v]<<endl;
	cout<<"點"<<G.apex[v]<<"的前驅頂點為: 點"<<G.apex[pre[v]]<<endl;
	cout<<"具體路徑為: "<<endl;

	int t = pre[v];           //終點的前驅下標
	//用棧儲存終點前驅們 一直到 源點
	stack<int> st;            
	while(t != u){
	    st.push(t);
	    t = pre[st.top()];
	}

    	cout<<G.apex[u];          		//源點
	while(!st.empty()){
	    t = st.top();
	    cout<<" --> "<<G.apex[t];   //中間點
	    st.pop();
        }
	cout<<" --> "<<G.apex[v]<<endl; //終點
	cout<<"———————————————————"<<endl;
    }
}


完整程式(含圖的鄰接矩陣)

#include<iostream>
#include<cstdio>
#include<stack>
using namespace std;
const int MAX = 100;
const int INF = 1e7;

typedef char ApexType;			//頂點名稱資料型別
typedef int EdgeType;			//邊權資料型別

typedef struct {

	ApexType apex[MAX];			//頂點表
	EdgeType edge[MAX][MAX];	//矩陣圖
	int nodenums, edgenums;		//頂點個數,邊個數

}Graph;

//建立鄰接矩陣
void CreateGraph(Graph *G){
    int i, j, k;
    int w;
    cout<<"輸入頂點個數和邊的條數: ";
    cin>>G->nodenums>>G->edgenums;
    //輸入頂點資訊
    for(i = 0; i < G->nodenums; i++){
	cout<<"輸入第 "<<i + 1<<" 個頂點的名稱: ";
	cin>>G->apex[i];
    }
    //初始化各頂點之間的邊為無窮大
    for(i = 0; i < G->nodenums; i++)
	for(j = 0; j < G->nodenums; j++)
	    G->edge[i][j] = INF;             
    //錄入有向邊的資訊
    for(k = 0; k < G->edgenums; k++){
	EdgeType w;
	cout<<"輸入<vi, vj>的對應點下標及權值: ";
	cin>>i>>j>>w;

        G->edge[i][j] = w;
    }
}

//列印圖的鄰接矩陣
void ShowGraphInMatrix(Graph *G){
    cout<<"   ";
    for(int i = 0; i < G->nodenums; i++)
	printf("%-4c",G->apex[i]);
    cout<<endl;

    for(int i = 0; i < G->nodenums; i++){
	printf("%-3c", G->apex[i]);
	for(int j = 0; j < G->nodenums; j++){
	    if(G->edge[i][j] == INF)
		cout<<"∞  ";
	    else
	        printf("%-4d", G->edge[i][j]);
	}
	cout<<endl;
    }		
}

//距離記錄陣列 , 前驅陣列
int dist[MAX], pre[MAX];  
//集合S標記陣列。如果flag[i]=true,說明該頂點i已經加入到集合S(最短路徑集合);否則i屬於集合V-S
bool flag[MAX];            

void Dijkstra(Graph *G, int u){
    for(int v = 0; v < G->nodenums; v++){
	dist[v] = G->edge[u][v];  //初始化源點u到各鄰接點v的距離
	flag[v] = false;
	if(dist[v] != INF)
	    pre[v] = u;           //若有鄰接邊,頂點v有前驅頂點u
	else
	    pre[v] = -1;          //若沒有,先初始化為-1
    }
    flag[u] = true;               //初始化集合S,只有一個元素: 源點u
    dist[u] = 0;                  //初始化源點u到自己的最短路徑為0
    
    /*   在集合V-S中尋找距離源點u最近的頂點t,使當前最短路徑樹最優  */
    for(int i = 0; i < G->nodenums; i++){
    	int tmp = INF, t = u;
    	for(int v = 0; v < G->nodenums; v++){
    	    if(!flag[v] && dist[v] < tmp){
    		//不在集合S中 並且 更小距離
    		t = v;            //記錄在V-S中距離源點u最近的頂點v
    		tmp = dist[v];
    	    }
    	}

    	if(t == u)
    	    return;               //未找到直接終止
    	flag[t] = true;           //否則, 將t加入集合S
        
        /*   更新集合V-S中與t鄰接的頂點到u的距離,擴充套件當前最短路徑樹  */
    	for(int v = 0; v < G->nodenums; v++){
    	    if(!flag[v] && G->edge[t][v] != INF){
    		//不在集合S中 且 有邊
                if(dist[v] > dist[t] + G->edge[t][v]){
                    //源點u可以藉助t到達v的距離更短
                    dist[v] = dist[t] + G->edge[t][v];
                    pre[v] = t;
                }
    	    }
    	}
    }
}

//還原源點u到各點具體路徑
void ShowShortParth(Graph G, int u){
    for(int v = 0; v < G.nodenums; v++){
	if(dist[v] == INF || dist[v] == 0)
	    continue;
	cout<<"\n點"<<G.apex[u]<<" 到 點"<<G.apex[v]<<" 的最短路徑距離為: "<<dist[v]<<endl;
	cout<<"點"<<G.apex[v]<<"的前驅頂點為: 點"<<G.apex[pre[v]]<<endl;
	cout<<"具體路徑為: "<<endl;

	int t = pre[v];           //終點的前驅下標
	//用棧儲存終點前驅們 一直到 源點
	stack<int> st;            
	while(t != u){
	    st.push(t);
	    t = pre[st.top()];
	}

    	cout<<G.apex[u];          		//源點
	while(!st.empty()){
	    t = st.top();
	    cout<<" --> "<<G.apex[t];   //中間點
	    st.pop();
        }
	cout<<" --> "<<G.apex[v]<<endl; //終點
	cout<<"———————————————————"<<endl;
    }
}


main(){
    Graph G;
    CreateGraph(&G);
    ShowGraphInMatrix(&G);
    
    int u;
    cout << "\n輸入出發的源點下標: ";
    cin>>u;

    Dijkstra(&G, u);
    
    cout<<"\n源點到所有點的單源最短路徑距離:"<<endl;
    ShowShortParth(G, v);
}


程式執行圖

圖的輸入和鄰接矩陣列印

單源最短路徑及具體路徑

相關文章