這篇介紹的是最小支撐樹,常見的有Prim算法和Krustal算法。
支撐樹:連通圖G的某一無環連通子圖T若覆蓋G中所有的頂點,則稱作G的一顆支撐樹或生成樹(spanning tree)。
支撐樹必須覆蓋所有的頂點,並且不能有環路,因此是禁止環路前提下的極大子圖,也是保持通路前提下的最小子圖。一個圖可能有很多支撐樹,它們都包含n個頂點和n-1條邊。
最小支撐樹:在帶權網絡G所有的支撐樹中,成本最低的稱為最小支撐樹(MST)。
Prim算法
割(cut):在圖G=(V;E)中,頂點集V的任一平凡子集U及其補集V\U構成一個割。如果邊uv滿足u屬於U且v不屬於U,稱作是該割的一條跨越邊(cross)。跨邊聯接於V及其補集之間,也稱作該割的一座橋。
Prim算法迭代的准則:最小支撐樹總是會采用聯接每一割的最短跨越邊。
算法的策略是貪心迭代:從一個點出發,每次都尋找已經得到的支撐樹子樹T'與剩余的點構成的割的最短跨越邊,並把它加入支撐樹。算法復雜度為O(n^2),適合於稠密圖、
考慮算法的過程,我們可以在發現一個頂點時,把它的優先級設置為與子樹T’的聯邊的權重。實際上,也就是把頂點的優先級設置為跨越邊的權重,然后尋找那個最短的跨越邊,加入到子樹中。因此,這個問題也可以歸於優先級搜索的框架:
1 template<typename Tv, typename Te> struct PrimPU 2 { 3 virtual void operator()(Graph<Tv, Te>* g, int uk, int v) 4 { 5 if (g->status(v) == UNDISCOVERED)//對於uk每個尚未被發現的鄰接頂點v 6 if (g->priority(v) > g->weight(uk, v)) 7 { 8 g->priority(v) = g->weight(uk, v);//更新優先級數 9 g->parent(v) = uk;//更新父節點 10 } 11 } 12 };
只需要重寫優先級更新器即可。
Krustal算法
前面的prim算法是以每一割的最短跨越邊來擴展支撐樹,通過更新點的優先級可以選取點和樹邊。
如果不考慮動態邊的情況,而是直接選擇邊的長度,似乎可以更加直觀一點。這也是krustal算法的思想。
把圖中所有的邊,按照權重進行排序,然后從最短的邊開始,把邊和它的兩個端點u和v加入到支撐樹中。然后按照邊的順序,繼續選擇最短邊,檢查兩個端點是否已經在支撐樹中,如果兩個點都在,那么舍棄這條邊繼續進行;只有一個端點在,把這條邊和新增的頂點加入支撐樹,此時可能會是兩個子樹的合並操作;兩個端點都不在,成為一個新的子樹。知道已經有n-1條邊時算法結束。
krustal算法在某些時候的效率可能會很高,但有時效率可能會很低,需要檢查完所有的邊才能結束算法,比如其他點之間的邊權重很小,而某一點只與很少的點有聯邊且聯邊權重很大的情況。
另外,krustal算法要求圖可以提供兩種基本操作:一種是並,即把兩個支撐樹子樹合並起來;另一個是查,檢查一個頂點是否在子樹中。支持這樣的操作,稱為並查集。
算法時間復雜度為O(eloge)。復雜度與邊的數量有關,因此,該算法適合邊比較稀疏的圖。
我沒有想到能把krustal納入到優先級搜索框架中的方法,因為krustal算法需要實現並查集的操作。可以考慮用這樣的結構來實現:把所有的頂點放在一個向量結構中,子集用多叉樹表示;當采用一條邊的時候,更新一個頂點的parent指針;在選擇邊的過程中,需要在各個子樹中進行查找操作;合並的操作即相當於兩棵樹的合並;最終的結果必然是得到了一個頂點作為根節點的多叉樹結構。
總結來看,兩種算法的最大不同之處,在於krustal對邊采取了排序操作,而prim算法沒有進行預處理。
