最短路徑問題
最短路問題是圖論中一種重要的演算法,本章將包括:
- 最短路徑問題
- 一.概念
- 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]\) 的值只與 \(dp[k-1][i][j]\) 有關,由是,我們可利用滾動陣列將 \(dp\) 陣列降維,來到二維,得到新的狀態轉移方程:
綜上,可得出 \(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\) 的應用場景很少,一般不會用到,可用來做小圖的小碼量選擇。