最小生成樹:Kruskal算法
概覽
Kruskal算法(克魯斯卡爾算法)是一種用來尋找最小生成樹的算法,由Joseph Kruskal在1956年發表。Kruskal算法是基於貪心的思想得到的。Kruskal算法在圖中存在相同權值的邊時有效。
問題
給定一個無向圖,如果它任意兩個頂點都聯通並且是一棵樹,那麼我們就稱之爲生成樹(Spanning Tree)。如果是帶權值的無向圖,那麼權值之和最小的生成樹,我們就稱之爲最小生成樹(MST, Minimum Spanning Tree)。
由Kruskal算法,我們可以求得帶權無向圖的最小生成樹的權值之和。
算法描述
算法思想
Kruskal算法是基於貪心的思想得到的。首先把所有的邊按照權值先從小到大排列,接着按照順序選取每條邊,如果這條邊的兩個端點不屬於同一集合,那麼就將兩個集合合併,直到所有的點都屬於同一個集合爲止。合併到一個集合時可以使用並查集優化。換言之,Kruskal算法就是基於並查集的貪心算法。
算法過程
- 記圖中有V個頂點,E條邊。
- 將所有邊按權值從小到大排序。排序完成後,我們選擇邊AD,這樣我們的圖就變成了下圖。
- 在剩下的邊裏繼續尋找權值最小的,這裏找到了CE,它的權值也是5。
- 依此類推找到DF,AB,BE。
- 下面繼續選擇到BC或EF。儘管現在長度爲8的邊是最小的未選擇的邊,但現在它們已經連通了(對於BC可以通過CE,EB連接,對於EF可以通過EB,BA,AD,DF連接),所以不需要選擇BC或EF。類似地,BD也不需要選擇。最後剩下EG和FG,選擇權值更小的EG。
算法的簡單證明
對圖的頂點數n做歸納,證明Kruskal算法對任意n階圖適用:
當n=1時,顯然能夠找到最小生成樹。
假設Kruskal算法對n=k階圖適用,那麼,在k+1階圖G中,把權值最小的邊的兩個端點u和v做合併操作,即把u與v合爲一個點v'
,把原來接在u和v的邊都接到v'
上去,這樣就能夠得到一個k階圖G'
,G'
的最小生成樹T'
可以用Kruskal算法得到。
下證T'+{<u,v>}
是G的最小生成樹。
用反證法,如果T'+{<u,v>}
不是最小生成樹,設最小生成樹是T,即W(T)<W(T'+{<u,v>})
。顯然T應該包含<u,v>
,否則,可以用<u,v>
加入到T中,刪除環上原有的任意一條邊,形成一棵更小權值的生成樹,而T-{<u,v>}
是G'
的生成樹(因爲G'
就是由G中的u,v兩點合併產生的),所以W(T')≤W(T-{<u,v>})
,即W(T'+{<u,v>})≤W(T)
。與W(T)<W(T'+{<u,v>})
矛盾。假設不成立。所以T'+{<u,v>}
是G的最小生成樹,Kruskal算法對k+1階圖也適用。
由數學歸納法,Kruskal算法得證。
程序代碼
#include <iostream>
#include <algorithm>
using namespace std;
const int node_num = 100; //最大頂點數
struct Edge
{
int u, v, w;
} e[node_num * node_num]; //邊的兩端點和權值
bool cmp(const Edge &a, const Edge &b)
{
return a.w < b.w;
}
int main()
{
int v_num; //頂點數
int a_num; //邊數
int sum = 0; //生成樹權值之和
int vset[node_num]; //記錄各頂點所在的樹
int k = 1; //記錄生成樹中頂點數量
cout << "v_num:";
cin >> v_num;
cout << "a_num:";
cin >> a_num;
for (int i = 0; i < v_num; ++i)
{
vset[i] = i; //初始化頂點所在的樹
}
for (int i = 0; i < a_num; ++i)
{
cin >> e[i].u >> e[i].v >> e[i].w;
}
sort(e, e + a_num, cmp); //按邊權值從小到大排列
for (int index = 0; index < a_num; ++index)
{
int sn1 = vset[e[index].u], sn2 = vset[e[index].v]; //讀取頂點所在的樹
if (sn1 != sn2) //如果不在同一個樹內
{
sum += e[index].w; //加上這條邊的權值
++k;
for (int i = 0; i < v_num; ++i) //遍歷每個頂點,把屬於sn2這棵樹的頂點全部加入sn1這棵樹
{
if (vset[i] == sn2)
{
vset[i] = sn1;
}
}
}
}
if (k == v_num)
{
cout << sum << endl;
}
else
{
cout << "E" << endl; //如果k不等於頂點數,說明不存在最小生成樹
}
return 0;
}
運行結果:
算法優化
並查集與路徑壓縮概述
並查集(Union Find Sets)是一種用於管理分組的數據結構。它具備兩個操作:查詢元素a和元素b是否爲同一組、將元素a和b合併爲同一組。
有一組指針指向每個節點的父節點,根節點的指針指向自己,通過循環就可以找到根節點。
路徑壓縮,就是在每次查找時,找到根節點,然後令查找路徑上的每個節點都直接指向根節點。
將a,b節點合併到同一棵樹,就是將a節點的根節點的父節點指針指向b節點的根節點。
實現
int find(int x) {//查找根節點
int p = x, t;
while (uset[p] != p){
p = uset[p];
}//返回根節點
while (x != p) {
t = uset[x];
uset[x] = p;
x = t;
}//路徑壓縮
return x;
}
例題
USACO agrinet / POJ 1258
描述
農民約翰被選爲他們鎮的鎮長!他其中一個競選承諾就是在鎮上建立起互聯網,並連接到所有的農場。當然,他需要你的幫助。
約翰已經給他的農場安排了一條高速的網絡線路,他想把這條線路共享給其他農場。爲了使花費最少,他想鋪設最短的光纖去連接所有的農場。你將得到一份各農場之間連接費用的列表,你必須找出能連接所有農場並所用光纖最短的方案。每兩個農場間的距離不會超過100000。
輸入輸出格式
第一行: 農場的個數,N(3<=N<=100)。
第二行..結尾:接下來的行包含了一個N*N的矩陣,表示每個農場之間的距離。當然,對角線將會是0,因爲線路從第i個農場到它本身的距離在本題中沒有意義。
只有一個輸出,是連接到每個農場的光纖的最小長度和。
樣例
SAMPLE INPUT:
4
0 4 9 21
4 0 8 17
9 8 0 16
21 17 16 0
SAMPLE OUTPUT:
28
程序代碼
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int node_num = 100;
struct Edge
{
int u, v, w;
} e[node_num * node_num];
bool cmp(const Edge &a, const Edge &b)
{
return a.w < b.w;
}
int main()
{
int v_num;
while (cin >> v_num)
{
int vi = 0, sum = 0, vset[node_num], k = 1;
for (int i = 0; i < v_num; ++i)
{
vset[i] = i;
for (int j = 0; j < v_num; ++j)
{
e[vi].u = i;
e[vi].v = j;
cin >> e[vi].w;
++vi;
}
}
sort(e, e + vi, cmp);
for (int index = 0; index < vi; ++index)
{
int sn1 = vset[e[index].u], sn2 = vset[e[index].v];
if (sn1 != sn2)
{
sum += e[index].w;
++k;
for (int i = 0; i < v_num; ++i)
{
if (vset[i] == sn2)
{
vset[i] = sn1;
}
}
}
}
cout << sum << endl;
}
return 0;
}
POJ 3723
描述
Windy有一個村莊,他想建立一支軍隊來保護他的村莊。他選了N個女孩和M個男孩,想讓他們做自己的士兵。在沒有任何關係的情況下,他僱傭一個士兵需要10000元。有一些女孩和男孩之間存在關係,Windy可以利用這些關係省一些花費。如果女孩x和男孩y間存在關係d且他們中的一個人已經被僱傭了,那麼Windy可以用10000-d元僱傭他們中的另一個。現在提供女孩和男孩之間的所有關係,你的任務是算出Windy最少要花多少錢。注意,僱傭一個士兵的時候最多隻能用一個關係。
輸入輸出格式
第1行:測試數據組數。
每組測試數據第1行:3個整數N,M,R。
下面R行,每行包含3個整數xi,yi,di。
在每組測試數據後有一個空行。
1≤N,M≤10000
0≤R≤50000
每組輸入數據對應一行輸出數據。
樣例
SAMPLE INPUT:
2
5 5 8
4 3 6831
1 3 4583
0 0 6592
0 1 3063
3 3 4975
1 3 2049
4 2 2104
2 2 7815 5 10
2 4 9820
3 2 6236
3 1 8864
2 4 8326
2 0 5156
2 0 1463
4 1 2439
0 4 4373
3 4 8889
2 4 3133
SAMPLE OUTPUT:
71071
54223
程序代碼
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
const int node_num = 20000;
int u[50000], v[50000], w[50000], pre[node_num], p[50000];
bool cmp(const int &a, const int &b)
{
return w[a] > w[b];
}
int findroot(const int);
int main()
{
int n;
scanf("%d", &n);
for (int N = 0; N < n; ++N)
{
int ngirl, nboy, vi, sum = 0;
scanf("%d%d%d", &ngirl, &nboy, &vi);
for (int i = 0; i < ngirl + nboy; ++i)
{
pre[i] = i;
}
for (int i = 0; i < vi; ++i)
{
p[i] = i;
}
for (int i = 0; i < vi; ++i)
{
int t;
scanf("%d%d%d", &u[i], &t, &w[i]);
v[i] = t + ngirl;
}
sort(p, p + vi, cmp);
for (int i = 0; i < vi; ++i)
{
int index = p[i];
int rootx = findroot(u[index]), rooty = findroot(v[index]);
if (rootx != rooty)
{
sum += w[index];
pre[rooty] = rootx;
}
}
sum = (ngirl + nboy) * 10000 - sum;
printf("%d\n", sum);
}
return 0;
}
int findroot(const int x)
{
if (pre[x] == x)
{
return x;
}
return pre[x] = findroot(pre[x]);
}