LeetCode—1489. 找到最小生成树里的关键边和伪关键边[Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree]——分析及代码[Java]
一、题目
给你一个 n 个点的带权无向连通图,节点编号为 0 到 n-1 ,同时还有一个数组 edges ,其中 edges[i] = [fromi, toi, weighti] 表示在 fromi 和 toi 节点之间有一条带权无向边。最小生成树 (MST) 是给定图中边的一个子集,它连接了所有节点且没有环,而且这些边的权值和最小。
请你找到给定图中最小生成树的所有关键边和伪关键边。如果从图中删去某条边,会导致最小生成树的权值和增加,那么我们就说它是一条关键边。伪关键边则是可能会出现在某些最小生成树中但不会出现在所有最小生成树中的边。
请注意,你可以分别以任意顺序返回关键边的下标和伪关键边的下标。
示例 1:
输入: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 的剩余边,所以它们是伪关键边。我们将它们作为输出的第二个列表。
示例 2 :
输入:n = 4, edges = [[0,1,1],[1,2,1],[2,3,1],[0,3,1]]
输出:[[],[0,1,2,3]]
解释:可以观察到 4 条边都有相同的权值,任选它们中的 3 条可以形成一棵 MST 。所以 4 条边都是伪关键边。
提示:
- 2 <= n <= 100
- 1 <= edges.length <= min(200, n * (n - 1) / 2)
- edges[i].length == 3
- 0 <= fromi < toi < n
- 1 <= weighti <= 1000
- 所有 (fromi, toi) 数对都是互不相同的。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-critical-and-pseudo-critical-edges-in-minimum-spanning-tree
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
二、分析及代码
1. Kruskal + 并查集
(1)思路
用 Kruskal 算法生成最小生成树,用并查集判断各节点间的连通情况。
因为伪关键边由一组或多组权重相同的边构成,可在各组相同权重的边中,通过枚举判断排除符合最小生成树要求的某条边后得到的节点连通情况与原来是否相同,判断该边为关键边或伪关键边。
(2)代码
class Solution {
public List<List<Integer>> findCriticalAndPseudoCriticalEdges(int n, int[][] edges) {
int size = edges.length;
int [] parent = new int[n];//并查集
for (int i = 0; i < n; i++)
parent[i] = i;
List<Edge> edgeList = new ArrayList<Edge>();//对边按权重排序
for (int i = 0; i < size; i++) {
edgeList.add(new Edge(i, edges[i][0], edges[i][1], edges[i][2]));
}
Collections.sort(edgeList, new Comparator<Edge>() {
public int compare(Edge e1, Edge e2) {
return e1.wei - e2.wei;
}
});
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> keyList = new ArrayList<>();//关键边集
List<Integer> pseudoList = new ArrayList<>();//伪关键边集
for (int i = 0; i < size; i++) {
int set = 0;
while (i + set + 1 < size && edgeList.get(i).wei == edgeList.get(i + set + 1).wei)//相同权重的边数
set++;
if (set == 0) {
if (union(parent, edgeList.get(i).p1, edgeList.get(i).p2)) {//只有一条边,如果在最小生成树中,就是关键边
keyList.add(edgeList.get(i).index);
}
continue;
} else {
int [] parentOld = new int[n];
copyArray(parent, parentOld, n);
for (int k = i; k <= i + set; k++) {//第一次遍历,记录新的连通情况
union(parent, edgeList.get(k).p1, edgeList.get(k).p2);
}
for (int k = i; k <= i + set; k++) {//第二次遍历
if (find(parentOld, edgeList.get(k).p1) == find(parentOld, edgeList.get(k).p2))//不在最小生成树中,直接排除
continue;
int [] parentNew = new int[n];
copyArray(parentOld, parentNew, n);//排除当前边,生成最小生成树
for (int j = i; j < k; j++)
union(parentNew, edgeList.get(j).p1, edgeList.get(j).p2);
for (int j = k + 1; j <= i + set; j++)
union(parentNew, edgeList.get(j).p1, edgeList.get(j).p2);
if (checkArray(parentNew, parent, n))//生成的最小生成树与包含当前边时相同,为伪关键边
pseudoList.add(edgeList.get(k).index);
else //生成的最小生成树改变,为关键边
keyList.add(edgeList.get(k).index);
}
}
i += set;
}
ans.add(keyList);
ans.add(pseudoList);
return ans;
}
public void copyArray(int[] oldList, int[] newList, int size) {
for (int i = 0; i < size; i++)
newList[i] = oldList[i];
}
public boolean checkArray(int[] list1, int[] list2, int size) {//检测最小生成树是否相同,需检测父节点值对应情况
for (int i = 0; i < size; i++)
if (find(list1, i) != find(list2, i))
return false;
return true;
}
//并查集部分
public int find(int[] parent, int index) {
if (parent[index] != index)
parent[index] = find(parent, parent[index]);
return parent[index];
}
public boolean union(int[] parent, int index1, int index2) {
int par1 = find(parent, index1);
int par2 = find(parent, index2);
if (par1 == par2)
return false;
if (par2 > par1)//统一以索引较小的节点为父节点,便于判断最小生成树是否相同
parent[par2] = par1;
else
parent[par1] = par2;
return true;
}
}
class Edge {
int index, p1, p2, wei;
public Edge(int index, int p1, int p2, int wei) {
this.index = index;
this.p1 = p1;
this.p2 = p2;
this.wei = wei;
}
}
(3)结果
执行用时 :13 ms,在所有 Java 提交中击败了 91.98% 的用户;
内存消耗 :39.3 MB,在所有 Java 提交中击败了 14.20% 的用户。
三、其他
更直观但低效的方法:计算最小生成树的总权重,直接枚举所有边,通过判断去掉该边后是否导致最小生成树无法生成或权值增加,确定该边为关键边或伪关键边。
更复杂但高效的方法:结合 Tarjan 算法求桥边。