算法简介:割舍怜悯之心 最小生成树,就是一张图一直删边,直到删成一颗树为止,这叫做生成树,如果这个生成树的边权之和是所有生成树中最小的,那么他被称为最小生成树,前面我们讲过了一个叫做 Boruvka 的算法,虽然他确实是求最小生成树的方法之一,但是他过于远古,不太常用,代码也不如 kruskal 简单,所以说,我们这里将介绍两种新的最小生成树算法,请看副标题 —— Prim & kruskal 。
算法演示:割舍侥幸之心 那么最小生成树,生成树生成树,总归就是树,有两种组成部分,一是点,二是边,而 Prim 和 kruskal 算法分别就是代表了基于点的贪心和基于边的贪心,我们来一个逐个击破。
点贪心:割舍痛苦之心 首先就是 Prim 算法,他之所以被称为点贪心,是因为这样一个思路:
生成树,必须要把一个图的所有点连起来,变成一个树,也就是说,所有点,我们都将出现在最终的点集合里。所以,我们一开始随便选择一个点,加入最小生成树点集合,然后每次去寻找和我们最小生成树点集合最近的一个点,这样就可以保证每次选出来的边是最短的,然后就一直循环往复,直到所有点都选完。
然后,我们就发现,这样的一个过程,和一个最短路算法极其神似,对,就是 算法。 算法的原理,就是每次从图里面拿出来一个最近的点去不断更新最短路。而 Prim 算法也是如此。我们可以看个图示:
那么首先根据第一个规则,我们随便选择一个点,比如 ,那么接下来挑选一个最近的点,显而易见是 边权只有 ,那么现在我们的集合里面有 然后最小生成树的大小是 。
接下来,我们扩张,发现从二出去的唯一一条边 太长了,不如从 到 的那条边,所以说我们扩张到 。现在点集里面有 然后最小生成树的大小是 。
以此类推,我们会接着扩张到 ,然后扩张到 然后扩张到 。然后最小生成树就建完了,总大小应该是 。
这样的话,我们就可以写出一个朴素版本的 Prim 代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int dis[N], vis[N], ans;inline void Prim () { dis[1 ] = 0 ; vis[1 ] = 1 ; ans = 0 ; dis[0 ] = 1e9 ; for (int i = 2 ; i <= n; i++) dis[i] = 1e9 ; for (int i = G.head (1 ); i; i = G.nt (i)) { dis[G.to (i)] = G.wt (i); } for (int i = 1 ; i < n; i++) { int mi = 0 ; for (int j = 1 ; j <= n; j++) { if (!vis[j] && dis[mi] > dis[j]) mi = j; } vis[mi] = 1 ; ans += dis[mi]; for (int j = G.head (mi); j; j = G.nt (j)) { int v = G.to (j); if (!vis[v] && dis[v] > G.wt (j)) { dis[v] = G.wt (j); } } } printf ("%d\n" , ans); }
然后这样要优化的话,也非常简单,依照 Dijstra 的方式优化即可,也就是用堆,优化掉求最小值的部分等。代码绝对不难写,留给读者。那么那个代码写出来的结果的话,一般来讲的复杂度就是 ,其中 为节点数, 为边的数量,不难发现,这个算法在对于稠密图的时候非常吃亏,因为稠密图的时候, 就将近与 了,这样再加上一个 复杂度绝对的爆炸存在。
边贪心:割舍封闭之心 其次又是万众瞩目的 kruskal 算法。kruskal 算法的优点在于,代码简短,复杂度相较于 Prim 略优。他被称为边贪心的原因也是因为他的思路,可是他的思路与刚刚的 Prim 恰恰相反。首先我们需要知道一个定理:
一个图中最短的边(如果唯一)一定在最小生成树中。
那么我们的发明者就开始想了,一共就只有 条边,那么我是不是选出来 条最短的边就可以了?不然。因为试想一下,如果我们 条最短的边无法把 个点连通,自然称不上最小生成树,也就是说,我们要在满足联通的情况下,尽可能地边权小。
那么想法也很简单,我们直接对于边权排序,每次合并一条最短的,两个端点原来不连通的相连通。这样合并 次,就是最小生成树。而这个未选点的判定,不难就想到用并查集。因为所谓原来不连通,是因为我们每次只挑最短的边,并不能保证顺序,也就是说,在合并结束前,我们的图一直处于四分五裂的状态。从来都不完整,这时候用并查集判断是不是在同一个连通块,再合适不过。
代码的话,我愿称之为这几个算法中最好写的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int fa[N];int find (int x) { return (fa[x] == x ? x : fa[x] = find (fa[x])); }struct Edge { int u, v, w; }e[M]; inline int kruskal () { int ans = 0 , cnt = 0 ; for (int i = 1 ; i <= n; i++) fa[i] = i; sort (e + 1 , e + m + 1 , [](Edge a, Edge b) { return a.w < b.w; }); for (int i = 1 ; i <= m; i++) { int u = find (e[i].u); int v = find (e[i].v); if (u == v) continue ; ans += e[i].w; fa[u] = v; cnt++; if (cnt == n - 1 ) break ; } return ans; }
这里统计一下,可以略略提升一点点的复杂度,这样的代码复杂度应该是 其中 是并查集的复杂度。
算法实战:割舍逢迎之心 查看相关算法标签喵!