最小生成树
1 什么是最小生成树
一个有n个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有n个结点,并且有保持图连通的最小的边。这就是图的最小生成树。最小生成树可以用Kruskal(克鲁斯卡尔)算法或prim(普里姆)算法求出。
1.1 具体描述
在一给定的无向图G=(V,E)中,(u,v)代表连接顶点u与顶点v的边,而w(u,v)代表此边的权重,若存在T为E的子集且为无循环图,使得W(T)最小,则此T为G的最小生成树。最小生成树其实是最小权重生成树的简称。
1.2 性质
设G=(V,E)是一个连通网络,U是顶点集V的一个非空真子集。若(u,v)是G中的一条一个端点在U中,另一个端点不在U中的边,且(u,v)具有最小权值,则一定存在G的一个最小生成树包括此边(u,v)。
1.3 算法描述
求MST的一般算法可描述为:针对图G,从空树T开始,往集合T中逐条选择并加入n-1条安全边(u,v),最终生成一棵含n-1条边的MST。当一条边(u,v)加入T时,必须保证T U {(u,v)}仍然是MST的子集,我们将这样的边称为T的安全边。
2 Prim算法
- 输入:一个加权连通图,其中顶点集合为V,边集合为E;
- 初始化:Vnew= {x},其中x为集合V中的任一节点(起始点),Enew= {},为空;
- 重复下列操作,直到Vnew=V;
- 在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
- 将v加入集合Vnew中,将<u, v>边加入集合Enew中;
- 输出:使用集合Vnew和Enew来描述所得到的最小生成树。
3 Kruskal算法
-
先构造一个只包含n个顶点,而边集为空的子图,若将该子图中各个顶点看成是各棵树上的根结点,则它是一个包含n棵树的一个森林;
-
之后,从网的边集E中选取一条权值最小的边,若该条边的两个顶点分属不同的树,则将其加入子图,也就是说,将这两个顶点分别所在的两棵树合成一棵树;反之,若该条边的两个顶点已落在同一棵树上,则不可取,而应该去下一条权值最小的边再试之。
-
依次类推,直至森林中只有一棵树,也即子图中含有n-1条边为止。
4 例题:(力扣:1489. 找到最小生成树里的关键边和伪关键边)
4.1 题目描述
给你一个 n 个点的带权无向连通图,节点编号为 0 到 n-1 ,同时还有一个数组 edges ,其中 edges[i] = [fromi, toi, weighti] 表示在 fromi 和 toi 节点之间有一条带权无向边。最小生成树 (MST) 是给定图中边的一个子集,它连接了所有节点且没有环,而且这些边的权值和最小。
请你找到给定图中最小生成树的所有关键边和伪关键边。如果从图中删去某条边,会导致最小生成树的权值和增加,那么我们就说它是一条关键边。伪关键边则是可能会出现在某些最小生成树中但不会出现在所有最小生成树中的边。
请注意,你可以分别以任意顺序返回关键边的下标和伪关键边的下标。
示例:
输入:n = 5, edges = [[0,1,1],[1,2,1],[2,3,2],[0,3,2],[0,4,3],[3,4,3],[1,4,6]]
输出:[[0,1],[2,3,4,5]]
解释:上图描述了给定图。
下图是所有的最小生成树。
注意到第 0 条边和第 1 条边出现在了所有最小生成树中,所以它们是关键边,我们将这两个下标作为输出的第一个列表。
边 2,3,4 和 5 是所有 MST 的剩余边,所以它们是伪关键边。我们将它们作为输出的第二个列表。
4.2 题目分析
根据题意,本题可用枚举+最小生成树判断,首先理解题目描述中的关键边和伪关键边的定义:
- 关键边:如果最小生成树中删除某条边,会导致最小生成树的权值和增加,那么我们就说它是一条关键边。也就是说,如果设原图最小生成树的权值为value,那么去掉这条边后会有两种情况:
- 要么整个图不连通,不存在最小生成树;
- 要么整个图连通,对应的最小生成树权值为v,其严格大于value
- 伪关键边:可能会出现在某些最小生成树中单不会出现在所有的最小生成树中的边。也就是说,我们可以再计算最小生成树的过程中,最优先考虑这条边,即最先将这条边的两个端点加入到并查集中合并。设最终得到的最小生成树权值为v,如果v=value,那么这条边就是伪关键边。
需要注意的是,关键边也满足伪关键边对应的性质。因此,我们首先对原图执行Kruskal算法,得到最小生成树的权值value,随后枚举每一条边,首先根据上面的方法判断其是否是关键边,如果不是关键边,再判断是否是伪关键边。
4.3 代码
class Solution {
public List<List<Integer>> findCriticalAndPseudoCriticalEdges(int n, int[][] edges) {
int m = edges.length; // 边的个数
int[][] newEdges = new int[m][4];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < 3; ++j) {
newEdges[i][j] = edges[i][j];
}
newEdges[i][3] = i;
}
Arrays.sort(newEdges, new Comparator<int[]>() {
public int compare(int[] u, int[] v) {
return u[2] - v[2];
}
});
// 计算 value
UnionFind ufStd = new UnionFind(n);
int value = 0;
for (int i = 0; i < m; ++i) {
if (ufStd.unite(newEdges[i][0], newEdges[i][1])) {
value += newEdges[i][2];
}
}
List<List<Integer>> ans = new ArrayList<List<Integer>>();
for (int i = 0; i < 2; ++i) {
ans.add(new ArrayList<Integer>());
}
for (int i = 0; i < m; ++i) {
// 判断是否是关键边
UnionFind uf = new UnionFind(n);
int v = 0;
for (int j = 0; j < m; ++j) {
if (i != j && uf.unite(newEdges[j][0], newEdges[j][1])) {
v += newEdges[j][2];
}
}
if (uf.setCount != 1 || (uf.setCount == 1 && v > value)) {
ans.get(0).add(newEdges[i][3]);
continue;
}
// 判断是否是伪关键边
uf = new UnionFind(n);
uf.unite(newEdges[i][0], newEdges[i][1]);
v = newEdges[i][2];
for (int j = 0; j < m; ++j) {
if (i != j && uf.unite(newEdges[j][0], newEdges[j][1])) {
v += newEdges[j][2];
}
}
if (v == value) {
ans.get(1).add(newEdges[i][3]);
}
}
return ans;
}
}
// 并查集模板
class UnionFind {
int[] parent;
int[] size;
int n;
// 当前连通分量数目
int setCount;
public UnionFind(int n) {
this.n = n;
this.setCount = n;
this.parent = new int[n];
this.size = new int[n];
Arrays.fill(size, 1);
for (int i = 0; i < n; ++i) {
parent[i] = i;
}
}
public int findset(int x) {
return parent[x] == x ? x : (parent[x] = findset(parent[x]));
}
public boolean unite(int x, int y) {
x = findset(x);
y = findset(y);
if (x == y) {
return false;
}
if (size[x] < size[y]) {
int temp = x;
x = y;
y = temp;
}
parent[y] = x;
size[x] += size[y];
--setCount;
return true;
}
public boolean connected(int x, int y) {
x = findset(x);
y = findset(y);
return x == y;
}
}