1. 程式人生 > >淺入淺出數據結構(25)——最小生成樹問題

淺入淺出數據結構(25)——最小生成樹問題

相交 其他 無向圖 基本 成本 不存在 自身 技術分享 回顧

  上一篇博文我們提到了圖的最短路徑問題:http://www.cnblogs.com/mm93/p/8434056.html。而最短路徑問題可以說是這樣的一個問題:路已經修好了,該怎麽從這兒走到那兒?但是在和圖有關的問題中,還有另一種有趣的問題:修路的成本已經知道了,該怎麽修路才能盡可能節約成本,同時將這些地方都連起來?

  比如我們知道有這麽幾個城市,它們互相之間還沒有路:

  技術分享圖片

  經過實地考察後,發現可以修的路以及各條路的修路成本如下:

  技術分享圖片

  但是我們的預算有限,需要在修路時盡可能的省錢(也就是盡量減小所有邊的權重之和),同時保證圖中每一個城市總是能到達圖中任意一個城市,該怎麽修路呢?對於上圖來說,其中一個方案是這樣的,其總共的修路成本(即總權重)為8:

  技術分享圖片

  另一個方案是這樣的,略有不同,不過總成本也是8:

  技術分享圖片

  像這樣的問題,就是我們今天要討論的最小生成樹問題。為了更準確地說明什麽是最小生成樹,我們需要先了解一個概念:連通。對於一個無向圖而言,如果每個頂點到每個其它頂點都存在路徑,則該無向圖是連通的。而對於有向圖而言,道理相同又稍有變化,在有向圖中,若每個頂點到每個其它頂點都存在可行的路徑,則該有向圖是強連通的。比如下圖就不是一個強連通的有向圖,其中非v0頂點無法到達v0頂點:

  技術分享圖片

  但是如果我們將上面這個有向圖的邊都變為無向邊,我們就會得到一個無向圖,此無向圖即該有向圖的基礎圖(underlying graph)。如果一個有向圖非強連通,但是其基礎圖是連通的,我們就稱該有向圖是弱連通的。上面這個有向圖就是一個弱連通的有向圖。

  明白了什麽是連通之後,接下來我們說說最小生成樹是什麽:在一個連通的無向圖的所有邊中,挑選出足以使所有頂點連通的那些邊,且這些邊的總權重不能更低,則這些邊與所有頂點構成的圖就是最小生成樹。“最小”的意思是其總權重是最小的,“生成”則是因為這個樹是從一個無向圖中找出來的,也即生成的。

  等等_(:з」∠)_ 不是說“這些邊與所有頂點構成的圖”嗎,怎麽就成了樹?原因是這樣的,如果一個無向圖是連通的,那麽我們就能找出滿足上述條件的那個圖,而如果那個圖存在,那它一定是一棵樹(樹是特殊的圖嘛,這一點應該要懂的),比如本文前面所找出的最小生成圖,顯然是一棵樹:

  技術分享圖片

  為什麽稱最後找出來的頂點與邊的集合為最小生成樹,我們已經知道了,而為什麽最後找出來的一定是樹……咱能不糾結嗎 ( ̄. ̄)

  好了,接下來討論下一個問題:有向圖可以找出最小生成樹嗎?答案是可以,只要有向圖是強連通的。並且尋找有向圖的最小生成樹的過程也是基本一樣的,因為無向圖本就是以有向圖的形式存儲的(一條無向邊拆成兩條有向邊)。不過因為本文並不打算給出可運行的代碼,所以我們的討論以無向圖為基準,主要關註算法的思路,並且不考慮所給圖非連通的情況。

  想要在圖中找出最小生成樹,有兩種算法可供選擇:Prim算法和Kruskal算法。因為Prim算法與尋找最短路徑的Dijkstra算法非常非常非常像,所以我們先來討論一下Prim算法。

  Prim算法的思路是這樣的:

  1.任選一個頂點,將其標為已知,即表示該頂點已在樹中(Dijkstra算法中,起點由我們指定)

  2.找出所有已知頂點鄰接的未知頂點,其中與任一已知頂點的鄰接邊權重最小的未知頂點,我們將其標為已知,同時將其preV設為與其鄰接邊最小的已知頂點,且其distance設為該鄰接邊的權重(在Dijkstra算法中,我們用的是“指向”,因為要考慮到有向圖的情況,此外,Dijkstra算法中,我們將被標為已知的未知頂點的distance設為與其相連的已知頂點的distance加上邊的權重)

  3.反復執行第二步,直至不存在已知頂點鄰接了未知頂點為止。

  抽象的說,Prim算法就是隨機選一個頂點,將其拉進原先為空的樹中,然後不斷地通過盡可能小的邊將其他頂點拉進這棵樹中

  

  老樣子,上述說法晦澀難懂 ( ̄. ̄)。所以我們需要實際的走一遍來加深一下理解,以下圖為例:

  技術分享圖片

  假設我們以v3作為起點,則圖初始化後的狀態如下(頂點旁有紅圈表示該頂點已知,紅圈中即該頂點的preV,頂點的distance我們暫不考慮):

  技術分享圖片

  接著,我們找出所有已知頂點(v3)鄰接的所有未知頂點:v0、v1、v2、v4、v5、v6。發現與已知頂點鄰接邊最小的未知頂點是v1、v4,其中未知頂點v1與已知頂點v3的鄰接邊權重為1,未知頂點v4與已知頂點v3的鄰接邊權重也為1,我們任選其一即可,比如選擇v1,然後將v1設為已知,v1.preV=v3:

  技術分享圖片

  繼續,我們找出所有已知頂點(v1、v3)鄰接的所有未知頂點:v0、v2、v4、v5、v6,發現與已知頂點鄰接邊最小的未知頂點是v0、v4,其中未知頂點v0與已知頂點v1的鄰接邊權重為1,未知頂點v4與已知頂點v1或v3的鄰接邊權重為1,我們任選其一,比如v4,然後將v4設為已知,v4.preV=v1(也可以是v4.preV=v3):

  技術分享圖片

  繼續,我們找出所有已知頂點(v1、v3、v4)鄰接的所有未知頂點:v0、v2、v5、v6,發現與已知頂點鄰接邊最小的未知頂點是v0、v6,其中未知頂點v0與已知頂點v1的鄰接邊權重為1,未知頂點v6與已知頂點v4的鄰接邊權重為1,我們選v6,將v6設為已知,v6.preV=v4:

  技術分享圖片

  繼續,我們找出所有已知頂點鄰接的所有未知頂點:v0、v2、v5,其中與已知頂點的鄰接邊最小的是v0,未知頂點v0與已知頂點v1的鄰接邊權重為1,我們將v0設為已知,v0.preV=v1:

  技術分享圖片

  繼續,找出所有已知頂點鄰接的所有未知頂點:v2、v5,發現其中未知頂點v5與已知頂點v6的鄰接邊權重最小為2,所以我們將v5設為已知,v5.preV=v6:

  技術分享圖片

  繼續,找出所有已知頂點鄰接的所有未知頂點:v2,其中未知頂點v2與已知頂點v5的鄰接邊權重最小,所以我們將v2設為已知,v2.preV=v5:

  技術分享圖片

  繼續,發現已經沒有哪個已知頂點鄰接了未知頂點,所以算法結束。

  接下來,我們只要進行這兩步操作就可以得出最小生成樹:

  1.將每個頂點與其preV相連的邊標為已知

  2.將非已知的邊刪去。

  將每個頂點與其preV相連的邊標為已知(註意,v3的preV是自身,此情況我們不做任何操作即可):

  技術分享圖片

  刪去非已知的邊:

  技術分享圖片

  當然,在實際編程中,有可能並不會執行這兩個操作,我們只要在將最小生成樹的相關信息保存在pathTable中即可,本例中算法結束後pathTable應為如下(與Dijkstra算法使用的pathTable略有不同:沒有distance域):

  技術分享圖片

  當然,我們也可以使用和Dijkstra算法時一樣的pathTable,即加上distance域,不過在計算最小生成樹時,一個頂點的distance域應該是其與preV鄰接的邊的權重:

  技術分享圖片

  在我們走一遍Prim算法時,我們發現v4.preV既可以設為v3,也可以設為v1,這就已經說明了一點:一個圖的最小生成樹並不一定是唯一的。不過還要註意的是:一個圖即便有多個最小生成樹,它們的總權重也應該是一樣的。

  如果你回顧一遍Prim算法和Dijkstra算法,就會發現,Prim算法與Dijkstra算法的區別可以說就兩個:

  1.Prim算法的“起點”是任選的,Dijkstra算法是給定的(畢竟要找的是單源最短路徑)

  2.Prim算法在將一個未知頂點設為已知時,其distance設為其與已知頂點的最小鄰接邊的weight,而Dijkstra算法則是設為已知頂點.distance+weight

  換句話說,Prim算法就是稍稍修改了一下的Dijkstra算法。如果你仔細觀察我們用Prim算法生成的樹,你會發現從v3出發到任意頂點的路徑恰好是v3到該頂點的最短路徑:

  技術分享圖片

  

  接下來本應討論Kruskal算法,但是我忽然發現我之前忘了寫一篇關於樹的集合與不相交集的博文(⊙?⊙)。如果要討論Kruskal算法,這兩個預備知識是必不可少的,而如果這兩個知識也要講解的話,博文就太長了Orz。所以我只簡單說說Kruskal的思路。

  在Prim算法中,我們是以已知頂點(即已在最小生成樹中的頂點)為基礎,不斷地將未知頂點拉進樹中。而Kruskal算法則是另一種思路:以當前最小的邊為基礎,不斷地將未知頂點拉進樹中(這個過程可能產生多棵樹)。

  以下圖為例,我們走一遍Kruskal算法:

  技術分享圖片

  首先,我們需要將邊按權重從小到大排序,才能找“當前最小邊”:

  技術分享圖片

  先是處理當前最小邊[v0,v1],其所連接的兩個頂點均未知,所以我們將它們均設為已知,並連起來:

  技術分享圖片

  然後處理下一個當前最小邊[v1,v3],其所連接的v3未知,將v3設為已知,連起來:

  技術分享圖片

  接著處理邊[v3,v4],其連接的v4未知,將v4設為已知,連起來:

  技術分享圖片

  接著處理邊[v1,v4],其連接的兩個頂點均為已知,故跳過

  接著處理邊[v4,v6],其連接的v6未知,將v6設為已知,連起來:

  技術分享圖片

  接著處理邊[v2,v5],其連接的v2和v5均為未知,所以將v2、v5均設為已知,連起來(註意,此時產生了兩棵樹):

  技術分享圖片

  接著處理邊[v5,v6],其連接的頂點均為已知,但是v5和v6處於不同的樹,所以我們將其連起來(這部分的相關判斷和處理需要樹的集合知識,以及不相交集數據結構):

  技術分享圖片

  接下來處理的所有邊都是所連接頂點已知,且所連接頂點處於同一棵樹中,所以均會跳過,然後算法結束。

  沒有掌握住博文的順序和鋪墊,實在是失敗 ̄△ ̄

  不過Kruskal算法的思路我想我應該講清楚了,就是Dijkstra算法和Prim算法的講解可能太生硬了一些,但是細細地讀、細細地理解、細細地過一遍,應該還是能明白的?ω?

  這個系列的博文的主體部分到這兒就算結束了,從第一篇博文一路看到這兒的話,基本的數據結構和算法應該都能掌握。而像什麽B+樹、紅黑樹、算法設計技巧等更特殊的知識我沒有算在主體部分中,以後可能會以“淺入淺出數據結構(附)”的標題形式寫出。

淺入淺出數據結構(25)——最小生成樹問題