文章目录
一、最小生成树
1.什么是最小生成树?
「树」和「图」的根本区别:树不会包含环,图可以包含环。
「生成树」就是在图中找一棵包含图中的所有节点的树。
如果一幅图没有环,完全可以拉伸成一棵树的模样。说的专业一点,树就是「无环连通图」。即生成树是含有图中所有顶点的「无环连通子图」。
比如下面这幅图,红色的边就组成了两棵不同的生成树:
一般来说,我们都是在无向加权图中计算最小生成树的,那么最小生成树就是所有可能的生成树中,权重和最小的那棵生成树。
2.如何判断图是否能生成树?
在这里我们使用前文 Union-Find 并查集算法运用 中介绍过的 Union-Find 算法,它在 Kruskal 算法中的主要作用是保证最小生成树的合法性(保证生成的是棵树,即不包含环)。
如以下条件:
给你输入编号从0到n - 1的n个结点,和一个无向边列表edges(每条边用节点二元组表示),请你判断输入的这些边组成的结构是否是一棵树。
对于这道题,我们可以思考一下,什么情况下加入一条边会使得树变成图(出现环)?
对于添加的这条边,如果该边的两个节点本来就在同一连通分量里,那么添加这条边会产生环;反之,如果该边的两个节点不在同一连通分量里,则添加这条边不会产生环。
输入:n = 5
edges = [[0,1],[1,2],[2,3],[1,3],[1,4]],但生成了不同的树。
无环:
有环:
// 判断输入的若干条边是否能构造出一棵树结构
boolean validTree(int n, int[][] edges) {
// 初始化 0...n-1 共 n 个节点
UF uf = new UF(n);
// 遍历所有边,将组成边的两个节点进行连接
for (int[] edge : edges) {
int u = edge[0];
int v = edge[1];
// 若两个节点已经在同一连通分量中,会产生环
if (uf.connected(u, v)) {
return false;
}
// 这条边不会产生环,可以是树的一部分
uf.union(u, v);
}
// 要保证最后只形成了一棵树,即只有一个连通分量
return uf.count() == 1;
}
class UF {
// 见上文代码实现
}
3.Kruskal 算法
树的判定算法加上按权重排序的逻辑就变成了 Kruskal 算法
所谓最小生成树,就是图中若干边的集合,你要保证这些边:
1、包含图中的所有节点。
2、形成的结构是树结构(即不存在环)。
3、权重和最小。
上文说到前两条可以地利用 Union-Find 算法做到,关键在于第 3 点,用贪心思路来保证得到的这棵生成树是权重和最小的:
将所有边按照权重从小到大排序,从权重最小的边开始遍历,如果这条边和mst中的其它边不会形成环,则这条边是最小生成树的一部分,将它加入mst集合;否则,这条边不是最小生成树的一部分,不要把它加入mst集合。最后mst集合中的边就形成了最小生成树
(mst是图中若干边的集合,Minimum Spanning Tree最小生成树的缩写)
二、例题
第一题:1135.最低成本联通所有城市
答案代码:
int minimumCost(int n, int[][] connections) {
// 城市编号为 1...n,所以初始化大小为 n + 1
UF uf = new UF(n + 1);
// 对所有边按照权重从小到大排序
Arrays.sort(connections, (a, b) -> (a[2] - b[2]));
// 记录最小生成树的权重之和
int mst = 0;
for (int[] edge : connections) {
int u = edge[0];
int v = edge[1];
int weight = edge[2];
// 若这条边会产生环,则不能加入 mst
if (uf.connected(u, v)) {
continue;
}
// 若这条边不会产生环,则属于最小生成树
mst += weight;
uf.union(u, v);
}
// 保证所有节点都被连通
// 按理说 uf.count() == 1 说明所有节点被连通
// 但因为节点 0 没有被使用,所以 0 会额外占用一个连通分量
return uf.count(