圖表算法—最小生成樹
1. 什麽是最小生成樹(Minimum Spanning Trees)
對於一個無向圖,如果它的所有邊都帶有一定的數值(即帶權),則會變成下面的樣子
假設這些點都是城市,每個城市之間的連線代表城市間的道路,線上的數字代表著道路的長短。當然,修越長的道路就需要越多的資源。
那麽如果要用最少的資源把所有城市都聯系起來(即任意城市A能沿著道路抵達任意城市B),我們應該怎樣建設道路呢?答案如下圖:
則就是最小生成樹:用最小的權值總和(即數值總和)把所有點都聯系起來。應註意到:最小生成樹的邊總數=此無向圖的點總數-1。
註意,最小生成樹裏是不應該有閉合循環的,如:
權值為24的那條邊顯然是多余的。
2. 生成帶權的無向圖
因為這種無向圖只比普通無向圖多了寫權值,我們只需在每條邊上多加一個變量來記錄權值即可。
使用的鄰接列表也需要把權值信息寫上,如下圖:
目前有兩種比較主流的算法來找最小生成樹:kruskal‘s algorithm(克魯斯卡爾算法)和Prim‘s algorithm(普林演算法)。(中文譯名采用音譯的方法。)
接下來,我們將逐一介紹這兩種算法。
3. kruskal‘s algorithm(克魯斯卡爾算法)
從例子入手:
為了容易理解,我們把所有邊按權值大小排成遞增的數組。實際代碼操作時,只需要寫個方法,讓數組輸出一個最小值即可。
我們需要創建幾個數組:
創建一個點的數組Points,把最小生成樹T的點存儲起來;
創建一個邊的數組mst(Minimum spanning tree的簡寫)來把最小生成樹的邊都存儲起來。
創建一個邊的數組pq把無向圖裏所有的邊都存儲起來。
首先數組pq輸出並移除一個最小值:0-7 0.16。
由於目前的最小生成樹T還沒有點,所以把0和7加進Points。
把0-7這條邊加進mst。
然後數組pq輸出並移除一個最小值:2-3 0.17。
檢查2和3是否在Points中。如果不在,則把這兩個點加入到Points中。
把2-3這條邊加進mst。
然後數組pq輸出並移除一個最小值:1-7 0.19。
7已經在Points中了,只需把1加進Points裏。
把1-7這條邊加進mst。
然後數組pq輸出並移除一個最小值:0-2 0.26。
由於0,2都已經在Points裏了,我們需要檢查如果把0-2這條邊加進最小生成樹裏是否會形成閉合循環。
檢查方法稍後介紹。
如果不會形成閉合循環,則把這條邊加進最小生成樹T中;否則,則無視這條邊,繼續看下一條邊。
在這裏,不形成閉合循環,把0-2這條邊加進mst中。
然後數組pq輸出並移除一個最小值:5-7 0.28。
7已經在Points中了,只需把5加進Points裏。
把5-7這條邊加進mst。
然後數組pq輸出並移除一個最小值:1-3 0.29。
由於1,3都已經在Points裏了,我們需要檢查如果把1-3這條邊加進最小生成樹裏是否會形成閉合循環。
會形成閉合循環,無視之。
然後數組pq輸出並移除一個最小值:1-5 0.32。
由於1,5都已經在Points裏了,我們需要檢查如果把1-5這條邊加進最小生成樹裏是否會形成閉合循環。
會形成閉合循環,無視之。
然後數組pq輸出並移除一個最小值:2-7 0.34。
2,7都已經在Points裏,且會形成閉合循環,無視之。
然後數組pq輸出並移除一個最小值:4-5 0.35。
5已經在Points中了,只需把4加進Points裏。
把5-4這條邊加進mst。
然後數組pq輸出並移除一個最小值:2-1 0.36。
2,1都已經在Points裏,且會形成閉合循環,無視之。
然後數組pq輸出並移除一個最小值:4-7 0.37。
4,7都已經在Points裏,且會形成閉合循環,無視之。
然後數組pq輸出並移除一個最小值:4-0 0.38。
4,0都已經在Points裏,且會形成閉合循環,無視之。
然後數組pq輸出並移除一個最小值:6-2 0.4。
2已經在Points中了,只需把6加進Points裏。
把6-2這條邊加進mst。
此時,mst裏的邊數=無向圖的點總數-1。說明最小生成樹已經形成了,算法結束。
現在討論如何檢測新加入的邊(v-w)是否會使最小生成樹形成閉合循環。假設現在最小生成樹的點總數為V。
可以用深度優先搜索來檢測v是否能抵達w,如果可以,說明最小生成樹中已經有路連接v和w了,再加一條v-w會形成閉合循環。
但是,還有一種方法更為高效:並查集算法。簡單總結一下就是:
v與和v相連的所有點形成一個區域,此區域用一個點a來代表。
w與和w相連的所有點形成一個區域,此區域用一個點b來代表。
然後對比a是否等於b,如果是,則說明v,w處於同一區域,最小生成樹中已經有路連接v和w了,再加一條v-w會形成閉合循環;如果不是,說明v和w處於不同區域,可以加v-w進最小生成樹。
總結一下kruskal‘s algorithm(克魯斯卡爾算法)的通用思路就是:
把所有邊放進一個數組裏。
然後數組輸出並移除一個擁有最小權值的邊,如果這條邊加入到最小生成樹內不會形成閉合循環,則把它加進去;否則無視它。
如此循環,直到最小生成樹的邊總數=無向圖的點總數-1為止。
順帶一提:輸出數值的最小值的方法可以把此數組做出最小堆的結構,然後輸出第一個元素即可。
代碼大概是這樣子的:
4. Prim‘s algorithm(普林演算法)
此算法有兩種實現版本:懶惰算法版(lazy implementation)和貪心算法版(eager implementation)。
懶惰算法和貪心算法是兩種思想,貪心算法也被稱為過度熱情算法。
從一個例子中感受一下:
假設a在他的房間裏愉快地玩耍,突然他媽叫他收拾房間。此時a有兩個選擇:要麽馬上收拾,要麽等會再收拾。
如果選擇馬上收拾,a會馬上打掃房間,甚至把走廊也掃了一遍。這就是貪心算法。
如果選擇等會再收拾,a會等到他媽要來檢查的前一瞬間才開始收拾。這就是懶惰算法。
對於一個程序來說,貪心算法就是當程序收到一個指令時,它不但會完成指令,還會過度熱情地多做點其它事情;
懶惰算法就是程序收到一個指令時,它會暫時無視之,等到我們要用到那個指令生成的結果時,程序才會去做這個指令。
接下來,逐一介紹這兩個實現版本:
普林演算法之懶惰實現版本
從例子入手:
我們需要創建幾個數組:
創建一個點的數組Points,把最小生成樹T的點存儲起來;
創建一個邊的數組mst(Minimum spanning tree的簡寫)來把最小生成樹的邊都存儲起來。
創建一個邊的數組pq。
首先,隨便找一個點開始:如0.
把0加入到Points裏,把含點0的所有邊加入到pq裏。(為了方便理解,這裏的邊按權值遞增排進數組,實際操作只需從數組中輸出最小值,不必排序)
然後pq輸出並移除最小一個最小值:0-7 0.16。
0已經在Points中了,只需把7加進Points裏。
把含點7的除了邊的另一個端頂點已經在Points裏之外的所有邊加入到pq裏。
然後pq輸出並移除最小一個最小值:1-7 0.19。
7已經在Points中了,只需把1加進Points裏。
把含點1的除了邊的另一個端頂點已經在Points裏之外的所有邊加入到pq裏。
然後pq輸出並移除最小一個最小值:0-2 0.26。
0已經在Points中了,只需把6加進Points裏。
把含點2的除了邊的另一個端頂點已經在Points裏之外的所有邊加入到pq裏。
然後pq輸出並移除最小一個最小值:2-3 0.17。
2已經在Points中了,只需把3加進Points裏。
把含點3的除了邊的另一個端頂點已經在Points裏之外的所有邊加入到pq裏。
然後pq輸出並移除最小一個最小值:1-3 0.29。
3,1都已經在Points裏,無視之。
然後pq輸出並移除最小一個最小值:7-5 0.28。
7已經在Points中了,只需把5加進Points裏。
把含點5的除了邊的另一個端頂點已經在Points裏之外的所有邊加入到pq裏。
然後pq輸出並移除最小一個最小值:1-5 0.32。
5,1都已經在Points裏,無視之。
然後pq輸出並移除最小一個最小值:7-2 0.34。
7,2都已經在Points裏,無視之。
然後pq輸出並移除最小一個最小值:4-5 0.35。
5已經在Points中了,只需把4加進Points裏。
把含點4的除了邊的另一個端頂點已經在Points裏之外的所有邊加入到pq裏。
然後pq輸出並移除最小一個最小值:1-2 0.36。
2,1都已經在Points裏,無視之。
然後pq輸出並移除最小一個最小值:7-4 0.37。
7,4都已經在Points裏,無視之。
然後pq輸出並移除最小一個最小值:0-4 0.38。
0,4都已經在Points裏,無視之。
然後pq輸出並移除最小一個最小值:2-6 0.4。
2已經在Points中了,只需把6加進Points裏。
把含點6的除了邊的另一個端頂點已經在Points裏之外的所有邊加入到pq裏。(沒有可加的邊)
此時,mst裏的邊數=無向圖的點總數-1。說明最小生成樹已經形成了,算法結束。
這個算法懶惰在哪呢?我們沿用那個收拾房間的例子。
總結一下通用思路:
1. 首先隨便選一個點加進Points
2. 然後把含此點的除了邊的另一個端頂點已經在Points裏之外的所有邊加入到pq裏。(孩子a聽到要收拾房間,就把東西全部塞到房間裏了)
3. 然後pq輸出並移除最小一個擁有最小值權值的邊,如果此邊的另一端點在Points裏,則無視之;如果不在,則把此點加進Points裏。(a的媽媽要來查房了,馬上收拾。)
4. 重復2,3步直到mst裏的邊數=無向圖的點總數-1。
代碼實現:
普林演算法之貪心實現版本
從例子入手:
我們需要創建幾個數組:
創建一個點的數組Points,把最小生成樹T的點存儲起來;
創建一個邊的數組mst(Minimum spanning tree的簡寫)來把最小生成樹的邊都存儲起來。
創建一個點的數組pq。
首先,隨便找一個點開始:如0.
把0加入到Points裏,把點0能直接去的點加入到pq裏。(為了方便理解,這裏的邊按權值遞增排進數組,實際操作只需從數組中輸出最小值,不必排序)
然後數組輸出並移除最小值:7. 把7對應的邊7-0加入到mst裏,把7加入Points裏。
點7能直接去的、不在Points裏的點(1,2,5,4)加入到pq,但是2,4已經在pq裏了,7-2的權值0.34比pq裏面的0-2的權值大,故無視之;7-4的權值0.34比pq裏面的0-4的權值大,故無視之。
然後數組輸出並移除最小值:1. 把1對應的邊7-1加入到mst裏,把1加入Points裏。
點1能直接去的、不在Points裏的點(3,2,5)加入到pq,但是2,5已經在pq裏了,1-2的權值0.36比pq裏面的0-2的權值大,故無視之;1-5的權值0.32比pq裏面的7-5的權值大,故無視之。
然後數組輸出並移除最小值:2. 把2對應的邊0-2加入到mst裏,把2加入Points裏。
點2能直接去的、不在Points裏的點(3,6)加入到pq,但是3,6已經在pq裏了,3-2的權值0.17比pq裏面的1-3的權值小,故取代之;6-2的權值0.4比pq裏面的0-6的權值小,故取代之。
然後數組輸出並移除最小值:3. 把3對應的邊2-3加入到mst裏,把3加入Points裏。
點3能直接去的、不在Points裏的點(6)加入到pq,但是6已經在pq裏了,3-6的權值0.52比pq裏面的6-2的權值大,故無視之。
然後數組輸出並移除最小值:5. 把5對應的邊7-5加入到mst裏,把5加入Points裏。
點5能直接去的、不在Points裏的點(4)加入到pq,但是4已經在pq裏了,5-4的權值0.35比pq裏面的0-4的權值小,故取代之。
然後數組輸出並移除最小值:4. 把4對應的邊5-4加入到mst裏,把4加入Points裏。
點4能直接去的、不在Points裏的點(6)加入到pq,但是6已經在pq裏了,4-6的權值0.93比pq裏面的6-2的權值大,故無視之。
然後數組輸出並移除最小值:6. 把6對應的邊6-2加入到mst裏,把6加入Points裏。
點6沒有能直接去的、不在Points裏的點。最小生成樹生成完畢,結束算法。
總結一下通用思路:
1. 首先隨便選一個點加進Points。
2. 然後把這個點能直接去的、不在Points裏的點加入到數組pq中,把對應的那些邊記錄下來。如果要加的點已經存在於pq中,則看新加入的對應的邊權值是否比已存在的小,如果是則取代之;否則無視之。
3. 數組輸出並移除擁有最小值的點,把這個點加進Points,這個點對應的邊加進mst裏。
4. 重復2-3步,直到pq沒有元素為止。
這個算法貪心在哪?
與懶惰版對比一下就很顯然了: 第二步,懶惰版是直接把所有邊塞進數組裏,而貪心版是全部逐一比較(a會馬上打掃房間,甚至把走廊也掃了一遍)。
圖表算法—最小生成樹