最短路徑問題

adsd45666發表於2024-06-23

最短路徑問題

最短路問題是圖論中一種重要的演算法,本章將包括:

目錄
  • 最短路徑問題
    • 一.概念
      • 1.概念
      • 2.解決方案
    • 二. \(Flord\) 演算法
      • 1.演算法思想
      • 2.程式碼詳解
      • 3.演算法應用及侷限性
    • 二. \(Djikstra\) 演算法
      • 1.演算法思想
      • 2.程式碼詳解
      • 3.演算法特徵及其侷限性
    • 三. \(Bellman-ford\) 演算法
      • 1.演算法思路
      • 2.程式碼詳解
      • 3.演算法特性
    • 四. \(SPFA\) 演算法
      • 1.演算法思想
      • 2.程式碼詳解
      • 3.特徵及性質
    • 五.總結

一.概念

1.概念

一張圖中n條點和m條邊,邊都有權值,權值可正可負。邊可能有向,可能無向,給定起點s和終點t,在所有能連結s和t的路徑中,尋找所經權值最小的路徑,此為最短路徑問題。

2.解決方案

最容易想到的方法是暴力法,列舉所有路徑,再進行大小比較,但明顯不可行,一定會超時。

更好的方法即為在尋路的過程中,動態規劃將要走的最短路徑,此也為下文的演算法思路和方法。

二. \(Flord\) 演算法

\(Flord\) 演算法是最簡單的最短路徑演算法,甚至短於暴力搜尋。

1.演算法思想

\(i\)\(j\) 的最短路徑,對於其他所有點,每個點 \(k\) 都嘗試一遍能否 \(i\) 借道 \(k\)\(j\) 會不會更短。

對於這樣的思路,我們可以用動態規劃來解決,定義 \(dp[k][i][j]\) ,表示 \(k\) 階段 \(i\)\(j\) 的最短路,不難發現 \(dp[k][i][j]\) 是由 \(dp[k-1][i][j]\) 推出,1.若不變,則直接繼承。2.若變,則是其加借道的權值即可。由是,我們便可推出其狀態轉移方程:

\[dp[k][i][j]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j]) \]

由狀態轉移方程可知, \(dp[k][i][j]\) 的值只與 \(dp[k-1][i][j]\) 有關,由是,我們可利用滾動陣列將 \(dp\) 陣列降維,來到二維,得到新的狀態轉移方程:

\[dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]) \]

綜上,可得出 \(Flord\) 演算法的核心程式碼。

2.程式碼詳解

for(int k=1;k<=n;k++){
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
        }
    }
}

由於使用了滾動陣列,所以 \(k\) 迴圈必須在 \(i\)\(j\) 迴圈之外。

3.演算法應用及侷限性

對於 \(Flord\) 演算法來說,其能一次跑出所有點對之間的最短路,我們稱這種求所有點對之間的最短路的問題為多源最短路問題,對比其他演算法來說,解決多源最短路問題, \(Flord\) 演算法 \(O(n^3)\) 的均攤複雜度是最優秀的。

當然,對於求一組點對之間的最短路的單源最短路問題, \(Flord\) 演算法 \(O(n^3)\) 的複雜度便難以接受。

綜上, \(Flord\) 演算法適合解決 \((n<300)\) 的小圖上的最短路問題。

二. \(Djikstra\) 演算法

\(Djikstra\) 演算法基於 \(BFS\) 可以說“ \(Djikstra\) = \(BFS\) +貪心”

1.演算法思想

對於 \(Djikstra\) 演算法來說,主要為點之間的擴充套件,對於我們將要處理的點來說,我們將其所有鄰居加入優先佇列中。再在優先佇列中選擇最小(隊首的)的那條邊進行處理,優先佇列中存放的是各點到起點的距離。

經過手動模擬,可以得出,若一個點 \(A\) 在之前的處理中已確定到起點的最小值,則後續的處理與其無關,即對於一個點,若其已被確定,則其不會再入隊。

2.程式碼詳解

void dijkstra(int s){             //s為起點
    for(int i=1;i<=n;i++){dis[i]=inf;done[i]=false;}
    //初始化,dis[i]表示i點到起點的距離,done[i]表示i點已確定。
    dis[s]=0;            //起點到自己的距離為0
    priority<node>q;     //優先佇列,小根堆
    q.push(node(s,0));
    while(!q.empty()){
        node u=q.top();             //彈出距離最小的點
        q.pop();
        if(done[u.id]) continue;    //若此點已確定
        done[u.id]=true;
        for(int i=0;i<=e[u.id].size();i++){
            edge y=e[u.id][i];
            if(done[u.to]) continue;
            if(dis[v.to]>y.w+u.dis){  //若透過此點更短
                dis[y.to]=y.w+u.dis;
                q.push(node(y.to,dis[y.to]));
            }
        }
    }
}

3.演算法特徵及其侷限性

\(Djikstra\) 演算法是較為高效,穩定的最短路徑演算法,每次得出一條最短路徑,所以穩定,每次只更新一個點,所以高效。

當然, \(Djikstra\) 演算法也並非完美無缺,其也有其侷限性,那就是對於其將要處理的圖來說,其不能出現權值為負的情況,若有負邊權,則貪心不成立,即不能保證“全域性最優解由區域性最優解組成”的最優性定理,導致 \(Djikstra\) 演算法出現錯誤。

三. \(Bellman-ford\) 演算法

\(Bellman-ford\) 演算法是一種簡單的單元最短路演算法。

1.演算法思路

我們透過多次推出,來鬆弛(指修改到各點的最短距離)各點的最短距離,也就是說透過一步步地推出最遠的點的最近路徑。

對於我們進行的每一次鬆弛來說,對每一條線段進行遍歷,看線段端點經過此線段能否比之前距起點的距離更近,若是,則更新。由於考慮回退邊的存在,所以共進行 \(n\) 次操作。

2.程式碼詳解

\(Bellman-ford\) 演算法的主程式碼較少:

for(int k=1;k<=n;k++)
    for(int i=1;i<=m;i++)
        if(dis[v[i]]>dis[u[i]]+w[i])
            dis[v[i]]=dis[u[i]]+w[i]

注:保證在主程式碼之前初始化 \(dis\) 陣列,除 \(dis[s]\) 之外,全部初始化為 \(INF\)

3.演算法特性

由演算法思路得, \(Bellman-ford\) 演算法遍歷 \(n\) 條邊 \(m\) 條邊,其複雜度為 \(O(mn)\) ,雖然其能處理 \(Djikstra\) 處理不了的負邊權問題,但當 \(n\)\(m\) 都很大時, \(Bellman-ford\) 演算法的效率將會十分糟糕,不過這也引出了我們的的第四種演算法 \(SPFA\)

四. \(SPFA\) 演算法

\(SPFA\) 演算法基於 \(Bellman-ford\) 演算法的佇列最佳化版。

1.演算法思想

既然其是 \(Bellman-ford\) 演算法的最佳化,那麼基本的思想不會改變,我們需要找到如何最佳化 \(Bellman-ford\) ,對於 \(Bellman-ford\) 的每一次鬆弛來說,我們需要遍歷每一條邊,其實並沒有必要,對於一個其鄰居已經確定的點來說,重複遍歷它的每條邊是無意義的,我們只需對發生變化的點的鄰居進行維護即可,佇列完全適合,於是這便是 \(SPFA\) 演算法。

2.程式碼詳解

bool inq[maxn];              //inq[i]表示i已在佇列中
void spfa(int s){            //s為起點
    for(int i=1;i<=n;i++){dis[i]=inf;inq[i]=false;}  //dis[i]為i到起點的距離
    dis[s]=0;
    queue<int>q;
    q.push(s);
    inq[s]=true;
    while(!q.empty()){
        int u=q.front();
        q.pop();
        inq[u]=false;
        for(int i=0;i<e[u].size();i++){
            int v=e[u][i].to,w=e[u][i].w;
            if(dis[u]+w<dis[v]){
                dis[v]=dis[u]+w;
                if(!inq[v]){                //若v不在佇列中
                    inq[v]=true;
                    q.push(v);              //入隊
                }
            }
        }
    }
}

3.特徵及性質

\(SPFA\) 演算法節點的入隊數量決定了 \(SPFA\) 演算法的效率,由於不能確定重新入隊的節點數量,所以 \(SPFA\) 不穩定,最差情況下為 \(O(mn)\) ,此時我們便應選擇穩定的 \(Djikstra\) 演算法來解決問題。

當給定的圖如下圖時:

\(SPFA\) 的複雜度將十分糟糕,不如 \(Djikstra\) 。當然, \(SPFA\) 也有自己的優點,即允許負邊權的存在。其也可用來判斷負環。

五.總結

這四種演算法各有千秋。

問題 邊權 演算法 複雜度
非負數 \(Djikstra\) \(O((m+n)log_2n)\)
單源最短路 允許有負數 \(Bellman-ford\) \(<O(mn)\)
允許有負數 \(SPFA\) \(O(mn)\)
多遠最短路 允許有負數 \(Flord\) \(O(n^3)\)

其中,求多源最短路,則使用 \(Flord\) ,若求單源最短路且無負邊權,則推薦使用穩定的 \(Djikstra\) 。若有負邊權,則使用 \(SPFA\)\(Bellman-ford\) 的應用場景很少,一般不會用到,可用來做小圖的小碼量選擇。

相關文章