最小生成樹算法:Kruskal算法 Prim算法
阿新 • • 發佈:2018-08-01
ont 進行 != 路徑 最小 tmp inf init 標識
定義
對於連通的無向圖G(V,E),如果一個E的無環子集T,可以連接所有節點,並且又具有最小權重,稱樹g(V,T)為圖G(V,E)的最小生成樹。
概念
偽代碼
Kruskal算法和Prim算法均使用貪心策略實現,兩者的實現框架可由下列偽代碼表示,首先,是一些敘述時使用的概念。
集合A:某棵最小生成樹的子集。
安全邊:加入集合A又不會破壞A性質的邊。
begin
A初始為空
while(A未形成最小生成樹)
選擇一條安全邊。
將安全邊加入A。
end
算法正確性證明
主要使用 循環不變式進行證明(這個概念在算法導論中經常用到) 循環不變式: 在每遍循環開始之前,A是某棵最小生成樹的子集。 初始化:集合A直接滿足循環不變式 保持:算法循環中,選擇安全邊保證了該性質。 終止:當算法終止時,所有邊均屬於某棵最小生成樹,所以算法正確。
關於安全邊
預先的一些概念: 切割:無向圖G(V,E)的一個切割(S,V-S)是集合V的一個劃分。 橫跨:如果邊e屬於E,且e的一個端點屬於S,另一端點屬於V-S,則該邊橫跨切割(S,V - S) 尊重:如果一個E的子集A中不存在橫跨切割(S,V - S)的邊,則稱該切割尊重集合A. 輕量級邊:在橫跨一個切割的所有邊中,權重最小的邊稱為輕量級邊。 定理:對於無向連通圖G(V,E),A為E的子集且包含在在某可最小生成樹中,設集合(S,V-S)為圖G中尊重A的一個切割,若e為橫跨切割(S,V-S)的一條輕量級邊, 則e對於集合A是安全的,即e為集合A的一條安全邊。 (下面的證明類似讀書筆記,嚴謹的證明請參考算法導論) 證明:假設輕量級邊e兩個端點u,v,(e橫跨切割,所以u,v分別屬於S,V - S),假設A包含在最小生成子樹T(假設T不包含e)中,則 T中存在一條簡單路徑p由u到v, 並且T 存在一條邊e‘屬於該路徑並且橫跨該切割,現在刪除e‘並且加入e形成另一個生成樹T‘,因為權重e <= e‘ 所以 權重T‘ <= T;因為T為最小生成樹, 所以T‘也為最小生成樹。即加入邊e後集合A包含在最小生成樹T‘中,即邊e對於集合A為安全邊。 推論:對於無向連通圖G(V,E),A為E的子集且包含在在某可最小生成樹中, C(Vc,Ec)為森林G‘(V,A)(包含G所有頂點,以及集合A中邊,由A的定義,G‘無環,且包含多個連通分量,由此構成森林G‘)中的一個連通分量, 如果e連接C和其他連通分量的一條輕量級邊,則e對集合A是安全的。 證明:容易知道,切割(Vc,V - Vc)尊重A,由定理可得推論正確性。 Kruskal算法:集合A為森林。尋找安全邊的方式是,權重最小且連接兩個連通分量(樹)的邊。 Prim算法:集合A為樹。尋找安全邊的方式為,連接A與A之外節點的權重最小的邊。
Kruskal算法
主要使用並查集來查詢集合選擇的邊是否屬於同一連通分量。
偽代碼
begin
A初始為空
建立並查集
按照權重對邊進行升序排序。
按順序考察每條邊:
如果邊端點分別屬於不同連通分量,加入該邊。
end
代碼實現
主要實現了並查集(按秩合並 和 路徑壓縮)
int a[101]; int rk[101]; void init_set() { for(int i = 1; i <= 100; ++i) { a[i] = i; rk[i] = 1; } } int find_set(int x) { int p = x; while(a[p] != p) { p = a[p]; } int now = x; int tmp = 0; while(now != p) { tmp = a[now]; a[now] = p; now = tmp; } return p; } void merge(int xp ,int yp) { if(rk[xp] > rk[yp]) { a[yp] = xp; } else { a[xp] = yp; if(rk[xp] == rk[yp]) { ++rk[yp]; } } } struct Nod { int x,y,w;//分別表示邊的端點x,y和邊的權重w. Nod():x(0),y(0),w(0){} bool operator < (const Nod& tmp)const { return w < tmp.w; } }; int kruskal() { init_set(); int e = n*n;//邊的數量; sort(nod,nod + e); int s1,s2; for(int i = 1; i <= e; ++i) { s1 = find_set(nod[i].x); s2 = find_set(nod[i].y); if(s1 != s2) { merge(s1,s2); } } return ret; }
時間復雜度分析
O(ElgV)
詳細分析暫時跳過:
Prim 算法
#define INF 0x7fffffff
int in[101];
struct Nod
{
int id,key;//分別為節點的序號和集合A到該節點權重最小的邊的權重。
Nod():id(0),key(0){}
bool operator <(const Nod& tmp)const
{
return key > tmp.key;
}
};
int key[101];//集合A到該節點的邊的權重,不存在該邊,設為無窮大。
void prim(int r)
{
for(int i = 1; i <= n; ++i)
{
key[i] = INF;
in[i] = 0;
}
Nod tmp;
key[r] = 0;
tmp.id = r;
tmp.key = 0;
priority_queue<Nod> q;//使用優先隊列維護待選擇的節點。
q.push(tmp);
while(!q.empty())//操作1,取點
{
tmp = q.top();
q.pop();
if(key[tmp.id] < tmp.key)continue;
int id = tmp.id;
in[id] = 1;//標記為1,,標識屬於最小生成樹集合;
//操作2,考察邊。
for(int i = 1; i <= n; ++i)//邊數較少可使用鄰接邊實現,這裏使用矩陣實現,
{
if(!in[i] && a[id][i] < key[i])
{
key[i] = a[id][i];
tmp.id =i;
tmp.key = key[i];
q.push(tmp);
}
}
}
}
時間復雜度分析
操作1:使用優先隊列實現,時間復雜度為lg(V)
操作2:考察所有邊,時間復雜度O(E)
總的時間復雜度為:O(Elg(V));
主要參考算法導論,如有錯誤,懇請指正
最小生成樹算法:Kruskal算法 Prim算法