目录
一、题目描述
给你一个 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) 数对都是互不相同的。
二、解题思路
这道题是不太会做的,不太会的原因是最小生成树克鲁斯卡尔算法是没有问题的,关键在于对最小生成树做的题不多,不熟悉,对于关键边和伪关键边的判断做不到题解那么简洁有条理。
以下为力扣官方题解方法一(枚举+最小生成树判定)的记录,用作学习笔记。
我们首先需要理解题目描述中对于「关键边」和「伪关键边」的定义:
- 关键边:如果最小生成树中删去某条边,会导致最小生成树的权值和增加,那么我们就说它是一条关键边。也就是说,如果设原图最小生成树的权值为value,那么去掉这条边后:
- 要么整个图不连通,不存在最小生成树;
- 要么整个图联通,对应的最小生成树的权值为 v,其严格大于 value。
- 伪关键边:可能会出现在某些最小生成树中但不会出现在所有最小生成树中的边。也就是说,我们可以在计算最小生成树的过程中,最先考虑这条边,即最先将这条边的两个端点在并查集中合并。设最终得到的最小生成树权值为 v,如果 v = value,那么这条边就是伪关键边。
需要注意的是,关键边也满足伪关键边对应的性质。因此,我们首先对原图执行 Kruskal 算法,得到最小生成树的权值 value,随后我们枚举每一条边,首先根据上面的方法判断其是否是关键边,如果不是关键边,再判断其是否是伪关键边。
三、代码实现
照着官方题解复现了一遍,加了详细注释
#include <bits/stdc++.h>
using namespace std;
class UnionFind {
private:
vector<int> father;
int findFather(int x) {
if (x != father[x]) {
father[x] = findFather(father[x]);
}
return father[x];
}
public:
UnionFind(int n) {
for (int i = 0; i < n; i++) {
father.push_back(i);
}
cnt = n;
}
bool Union(int a, int b) {
int fa = findFather(a);
int fb = findFather(b);
if (fa == fb) return false;
father[fa] = fb;
cnt--;
return true;
}
bool isConnected(int a, int b) { return findFather(a) == findFather(b); }
~UnionFind() {}
//连通块数
int cnt;
};
static bool cmp(vector<int>& a, vector<int>& b) { return a[2] < b[2]; }
vector<vector<int>> findCriticalAndPseudoCriticalEdges(
int n, vector<vector<int>>& edges) {
int m = edges.size();
for (int i = 0; i < m; i++) {
//排序前先记录原始的位置
edges[i].push_back(i);
}
sort(edges.begin(), edges.end(), cmp);
int value = 0;
//原图的最小生成树的value值
UnionFind std_uf(n);
for (int i = 0; i < m; i++) {
if (std_uf.Union(edges[i][0], edges[i][1])) value += edges[i][2];
}
//结果数组
vector<vector<int>> res(2);
//关键边判断
//依次去掉每条边,用剩余的边构建最小生成树
//此时若图不连通或者v > value,则为关键边
for (int i = 0; i < m; i++) {
UnionFind now_uf(n);
int v = 0;
for (int j = 0; j < m; j++) {
if (i != j && now_uf.Union(edges[j][0], edges[j][1]))
v += edges[j][2];
}
if (now_uf.cnt != 1 || (now_uf.cnt == 1 && v > value)) {
res[0].push_back(edges[i][3]);
//若边为关键边,则伪关键边
//若边不是关键边,有可能是伪关键边
continue;
}
//判断伪关键边
//优先选一条边进行合并,作为最小生成树的一条边
//用剩余的边继续构建最小生成树,若v == value,为伪关键边
now_uf = UnionFind(n);
//先选择该边
now_uf.Union(edges[i][0], edges[i][1]);
v = edges[i][2];
//继续构建最小生成树
for (int j = 0; j < m; j++) {
if (i != j && now_uf.Union(edges[j][0], edges[j][1]))
v += edges[j][2];
}
if (v == value) {
res[1].push_back(edges[i][3]);
}
}
return res;
}
int main() {
int n = 4;
vector<vector<int>> edges = {{0,1,1},{1,2,1},{2,3,1},{0,3,1}};
vector<vector<int>> res = findCriticalAndPseudoCriticalEdges(n, edges);
for (auto&& i : res) {
for (auto&& x : i) {
cout << x << " ";
}
cout << endl;
}
return 0;
}