Background:
在无向图中,连通并且不含圈的图称为树(Tree)。给定无向图G = (V, E), 连接G中所有点,且边集是E的子集的树的生成树称为G的(Spanning Tree),而权值最小的生成树称为最小生成树(Minimal Spanning Tree, MST)。Kruscal算法就是构造最小生成树的一种算法。
Kruskal Algorithm:
Steps:
1. 给所有边按照从小到大的顺序排列。
2. 情况1: 如果u, v在同一个连通分量中,那么加入(u, v)之后会形成环,因此不能选择。
情况2: 如果u, v在不同的连通分量,那么加入(u, v)一定是最优的。用反证法:如果不加入这条边能得到一个最优解T,则T + (u, v)一定有且只有一个环,而且环中至少有一条边(u', v')的权值大于或者等于(u, v)的权值。删除该边之后,得到的新树T' = T + (u, v) - (u', v')不会比T更差。因此,加入(u, v)不会比不加入差。
伪代码:
将所有边排序,记第i小的边为e[i] (1 <= i < m)
初始化MST为空
初始化连通分量,让每一个点都自成一个连通分量
for (int i = 0; i < m; i++)
if (e[i].u 和 e[i].v 不在同一个连通分量) {
把边e[i]加入MST
合并e[i].u和e[i].v所在的连通分量
}
核心:如何快速检查e[i].u和e[i].v是否在同一个连通分量中。
Solution: Union-Find Set:
可以把每个连通分量看成一个集合,该集合包含了连通分量中的所有点。这些点两两相通,而具体的连通方式无关紧要,就好比集合中的元素没有先后顺序之分,只有“属于”和不“属于”的区别。
Union-Find Set的精妙之处在于用树表示集合。规定每棵树的根结点是这棵树所对应的集合的代表元(representative)。
int find(int x) { if (p[x] = x) return x; return find(p[x]); }
极端情况:这棵树是一条长长的链,假设链的最后一个结点是x,则每次执行find(x)都会遍历整个树,效率十分低下。
Solution: 改进find方法,只要把遍历过的结点都改成树根的字结点,下次查询的时候就会快很多。
Code:
int cmp(const int i, const j) { return w[i] < w[j]; }
int find(int x) { return p[x] == x ? x : p[x] = find(p[x]); }
int Kruskal() {
int ans = 0;
for (int i = 0; i < n; i++) p[i] = i;
for (int i = 0; i < m; i++) r[i] = i;
sort(r, r + m, cmp);
for (int i = 0; i < m; i++) {
//r[i] 排序后第i小的边
int e = r[i]; int x = find(u[e]); int y = find(v[e]);
if (x != y) { ans += w[e]; p[x] = y;}
}
return ans;
}
Note: 注意不能写成p[u[e]] = p[v[e]],因为u[e]和v[e]不一定是树根,所以不能这两棵树的连通。