對最小生成樹和最短路徑相關算法的簡要總結


最小生成樹和最短路徑都是圖論中比較基本的內容,我最開始在大學接觸時感覺懵懵懂懂的,後來工作之後重新看算法相關的東西算是都會編代碼了,但知道最近我重新把相關的內容深入學習了一遍才感覺把原理性的東西也弄明白了。當然,我所說的弄明白了只是最基本的內容。
以下只說我理解之後的乾貨,至於每個算法的詳細及通俗介紹,網上一搜一大片,我沒必要在這裏重複講了。

一、兩類基本問題

1.基本問題描述

最小生成樹是在連通圖中找出最少的邊將所有點聯通,使得邊的權值之和最小。所以對於有n個點的連通圖,其最小生成樹一定是有n-1條邊。但是由於原圖中邊的權值可能相同,找到的最小生成樹可能不是唯一的。
最短路徑是給定圖中兩點,求最短路徑。如果圖是不連通的,則最短路可能不存在或者說無窮大。通常是給定起點和終點求最短路徑,但一般算法都會順帶求出其他最短路徑,甚至floyd算法會一次性求出所有點兩兩之間的最短路徑。

2.基本使用條件

最小生成樹要求圖是連通圖。連通圖指圖中任意兩個頂點都有路徑相通,通常指無向圖。理論上如果圖是有向、多重邊的,也能求最小生成樹,只是不太常見。
最短路徑同樣要求圖是連通圖,否則有些點之間就不存在路徑了。通常是用於無向單重邊的圖。不同的算法對圖中是否有負邊或負權是有適用條件的,後面會再說。

二、最小生成樹常用算法

1.Prim算法

基本思路
是從任取一個點作爲初始集合開始,找出與集合中的點距離最小的外部點加入作爲新的集合,然後不斷重複直到所有點加入集合,不同點加入集合時對應的最短邊的集合就構成了最小生成樹。
性能分析
用V表示頂點數,E表示邊數。算法的外循環最多爲V-1次,如果圖用鄰接矩陣存儲則內循環也需要O(V)次,因而基本算法複雜度爲O(V^2)。
但是如果用堆結構來維護已訪問過的點的集合到未訪問過的點的距離時,時間複雜度可以優化到O(ElogV),這時內循環就沒有了,而變成了O(E)次存取邊的操作,而每次存取的時間複雜度爲logV(因爲用的是堆結構)。後面講到Dijkstra最短路徑算法也有類似的優化。
適用場景
通常圖爲稠密圖時用Prim算法相比Kruskal會比較好。
而上述優化的適用條件是圖爲稀疏圖,這時E遠小於V^2。如果是稠密圖,則此優化無意義。
所以稀疏圖情況下,使用堆的Prim算法與Kruskal算法複雜度相當。但是由於寫法上Kruskal算法更簡潔方便,所以Kruskal算法更常用。
延伸討論
用斐波那契堆能夠進一步降低複雜度到O(V * lgV)。參考博文Dijkstra算法與Prim算法的異同

2.Kruskal算法

基本思路
是將所有的邊排序,然後從最小的邊開始,只要滿足加入邊不構成環,就加入集合,直到加入n-1條邊,就構成了最小生成樹。
性能分析
算法分爲排序和主體兩步。對所有邊排序的複雜度爲O(ElogV),至於後面最小生成樹的主體部分,則是O(E)的,因爲Kruskal算法通常會配並查集數據結構來用於判環,該數據結構在穩定狀態下複雜度爲常數級。所以Kruskal算法整體複雜度爲O(ElogV)。
適用場景
更適用於稀疏圖。而實際情況中圖一般就是稀疏圖。

3.延伸內容

兩種算法的圖文解釋可以參考以下鏈接:
最小生成樹(Kruskal和Prim算法)

三、最短路徑常用算法

此部分基本框架參考了博文:最短路徑算法

1.Bellman-Ford算法

基本思路
對所有邊進行鬆弛操作V-1次,即可得到單源最短路徑。
關於鬆弛操作的深入理解請參考該博客。
性能分析
時效性較好,時間複雜度O(VE),其中外循環複雜度爲O(V),內循環複雜度爲O(E)。
適用場景
求單源最短路,允許有負權邊,但不能有負圈。不過如果有負圈,可以判斷出來,判斷的方法是第n次外循環最短路徑值仍然會更新。所以也可用於判斷負圈(若有則不存在最短路)。
有負權邊的情況下求最短路只能用Bellman-Ford算法。
延伸內容
1、Bellman-Ford算法的圖文解釋可參考博文:
Bellman-Ford最短路徑算法
Bellman-Ford算法詳解
2、對算法正確性的理解需要知道鬆弛操作的相關性質。可參考博文鬆弛操作的性質
3、算法的外循環其實可以提前終止,條件是所有鬆弛操作都沒有更新最短路徑。參考博文最短路算法 :Bellman-ford算法 & Dijkstra算法 & floyd算法 & SPFA算法 詳解

2.Dijkstra算法

基本思路
與最小生成樹Prim算法很類似,是將起點作爲初始集合開始,根據集合找出與起點距離最小的外部點加入作爲新的集合,然後不斷重複直到目標點加入集合。
後文會講兩種算法的區別。
性能分析
算法比較穩定,的時間複雜度可爲O(V^2)。用堆結構來已找到的最短距離時,時間複雜度可以優化到O(ElogV)。因而其時間複雜度跟最小生成樹Prim算法非常相似。
適用場景
最常用的最短路徑算法。求單源、無負權的最短路(顯然也不會有負圈)。
無負權邊的情況下適用,複雜度相對於Bellman-Ford算法更低。
另外據有的博客講,其實並不是必須無負權邊,有些特殊情況下有負權邊也能用。
延伸內容
1、據說該算法也可以用於判斷負環,參考博文關於dijkstra判斷負環的思考
2、Dijkstra算法其實是BFS(廣度優先搜索)的升級版,邊無權就是BFS,邊有權就成了Dijkstra。
關於dfs,bfs,Dijkstra的比較可參考博文簡述dfs,bfs,Dijkstra思想及區別
3、算法的圖文解釋可參考博文Dijkstra算法圖文詳解

3.Floyd算法

基本思路
Floyd-Warshall算法(Floyd-Warshall algorithm)是解決任意兩點間的最短路徑的一種算法,可以正確處理有向圖或負權的最短路徑問題。
求多源、無負權邊的最短路。是一個動態規劃算法特別經典以及簡潔,只有五行:
在這裏插入圖片描述
性能分析
用矩陣記錄圖。時效性較差,時間複雜度O(V^3)。
適用場景
求多對多的最短路徑時用floyd就一起求出來了。
也可以用於判斷是否有負圈,方法是看是否循環完畢存在d[i][i]爲負數的頂點i。
延伸內容
1、需要注意算法的寫法,中間點k的遍歷一定是在外循環。
2、算法的正確性理解與Bellman-Ford算法有些類似,可以參考博客floyd算法:我們真的明白floyd嗎?

還可以參考《挑戰程序設計競賽》第2版P103從動態規劃的思路所給的證明,但個人認爲該證明並不顯然,不如上一篇博文。

4.SPFA算法

相對用的少一些,雖然算法複雜度有時會更好,但據有些測試說複雜度不穩定。
其實是Bellman-Ford的隊列優化,時效性相對好,時間複雜度O(kE)。(k<<V)。
與Bellman-ford算法類似,SPFA算法採用一系列的鬆弛操作以得到從某一個節點出發到達圖中其它所有節點的最短路徑。所不同的是,SPFA算法通過維護一個隊列,使得一個節點的當前最短路徑被更新之後沒有必要立刻去更新其他的節點,從而大大減少了重複的操作次數。

5.延伸總結

求單源最短路最常用Dijkstra,如果有負權可以用Bellman-Ford,判斷負圈可以用Bellman-Ford。
求多源最短路用Floyd,也可用於判負圈。
在有些特殊情況下可用SPFA,但不太常用。

四、不同算法的對比

這裏把最小生成樹和最短路徑算法統一起來說,有一些有趣的對比。

1.Dijkstra和Prim算法的異同

Prim算法是計算最小生成樹的算法,而Dijkstra算法是計算最短路徑的算法,二者看起來比較類似。假設全部頂點的集合是V,已經被挑選出來的點的集合是U,那麼二者都是從集合V-U中不斷的挑選權值最低的點加入U,而且前面的算法複雜度分析也表明兩者非常像,那麼二者是否等價呢?也就是說是否Dijkstra也可以計算出最小生成樹而Prim也可以計算出從第一個頂點v0到其他點的最短路徑呢?答案是否定的。
那麼兩者的根本區別是什麼呢?

(1)博文Prim和Dijkstra算法的區別解釋說:
二者的不同之處在於“權值最低”的定義不同,Prim的“權值最低”是相對於U中的任意一點而言的,也就是把U中的點看成一個整體,每次尋找V-U中跟U的距離最小(也就是跟U中任意一點的距離最小)的一點加入U;而Dijkstra的“權值最低”是相對於v0而言的,也就是每次尋找V-U中跟v0的距離最小的一點加入U。
一個可以說明二者不等價的例子是有四個頂點(v0, v1, v2, v3)和四條邊且邊值定義爲(v0, v1)=20, (v0, v2)=10, (v1, v3)=2, (v3, v2)=15的圖,用Prim算法得到的最小生成樹中v0跟v1是不直接相連的,也就是在最小生成樹中v0v1的距離是v0->v2->v3->v1的距離是27,而用Dijkstra算法得到的v0v1的距離是20,也就是二者直接連線的長度。

(2)另一篇博文Dijkstra算法與Prim算法的異同從鬆弛操作的角度進行了另一種解釋(本質上與前面一致):
兩個僞算法的差別只在於最後循環體內的鬆弛操作。

  • 最小生成樹只關心所有邊的和最小,所以有v.key = w(u, v),即每個點直連其他點的最小值(最多隻有兩個節點之間的權值和)
  • 最短路徑樹只搜索權值最小,所以有v.key = w(u, v) + u.key,即每個點到其他點的最小值(最少是兩個節之間的權值和)

這兩篇博文其實說的很清楚了,Dijkstra算法與Prim算法雖然面向的是不同問題,但在思路上和複雜度上是非常相似的,唯一的核心區別是對集合外的點,是考慮到集合中點的最短距離(Prim算法),還是隻考慮到起點的最短距離(Dijkstra算法)。

2.不同算法複雜度的對比

Prim和Dijkstra算法的複雜度在用二叉堆優化後都能到O(ElogV)。Kruskal算法本身的複雜度就是O(ElogV),主要耗費在排序上。
Bellman-Ford算法的複雜度是O(VE),複雜度高一些,但能解決負權和負圈問題。
Floyd算法複雜度是O(V^3),寫法簡單,適用於解決多對多最短路徑。

五、關於數據結構

常用數據結構有三種:鄰接矩陣、鄰接表以及前向星。(參考博文:圖的存儲 鄰接矩陣+鄰接表+鏈式前向星

1.鄰接矩陣

在樹的問題中,鄰接矩陣是空間、時間的極大浪費。 假設樹的結點個數爲 N = 100000。
建立鄰接矩陣需要空間爲 1e5*1e5 但是由於只有 N - 1 條邊,所以在鄰接矩陣中只有 100000 - 1 個有效 信息。
即只利用了鄰接矩陣的 0.00001%,剩餘空間全部被浪費。

2.鄰接表

鄰接表是最常用存儲結構之一。 但是 vector(動態數組) 的時間效率較低 (較普通數組而言)。
那有沒有一種用普通數組可以存儲, 時間和空間都極佳的存儲結構?

3.鏈式前向星

鏈式前向星是介於 鄰接矩陣 和 鄰接表 之間比較均衡的一種數據結構。可參考博文: 對於“前向星”的理解

4.延伸比較

鄰接矩陣與鄰接表相比,疏鬆圖多用鄰接表,稠密圖多用鄰接矩陣。

另:還可參考博文圖的存儲結構:鄰接矩陣(鄰接表)&鏈式前向星

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章