题目:
给你一个 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 条边都是伪关键边。
代码:
// 并查集模板
class UnionFind {
public:
vector<int> parent;
vector<int> size;
int n;
// 当前连通分量数目
int setCount;
public:
UnionFind(int _n): n(_n), setCount(_n), parent(_n), size(_n, 1) {
iota(parent.begin(), parent.end(), 0);
}
int findset(int x) {
return parent[x] == x ? x : parent[x] = findset(parent[x]);
}
bool unite(int x, int y) {
x = findset(x);
y = findset(y);
if (x == y) {
return false;
}
if (size[x] < size[y]) {
swap(x, y);
}
parent[y] = x;
size[x] += size[y];
--setCount;
return true;
}
bool connected(int x, int y) {
x = findset(x);
y = findset(y);
return x == y;
}
};
// Tarjan 算法求桥边模版
class TarjanSCC {
private:
const vector<vector<int>>& edges;
const vector<vector<int>>& edgesId;
vector<int> low;
vector<int> dfn;
vector<int> ans;
int n;
int ts;
private:
void getCuttingEdge_(int u, int parentEdgeId) {
low[u] = dfn[u] = ++ts;
for (int i = 0; i < edges[u].size(); ++i) {
int v = edges[u][i];
int id = edgesId[u][i];
if (dfn[v] == -1) {
getCuttingEdge_(v, id);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) {
ans.push_back(id);
}
}
else if (id != parentEdgeId) {
low[u] = min(low[u], dfn[v]);
}
}
}
public:
TarjanSCC(int n_, const vector<vector<int>>& edges_, const vector<vector<int>>& edgesId_): \
edges(edges_), edgesId(edgesId_), low(n_, -1), dfn(n_, -1), n(n_), ts(-1) {}
vector<int> getCuttingEdge() {
for (int i = 0; i < n; ++i) {
if (dfn[i] == -1) {
getCuttingEdge_(i, -1);
}
}
return ans;
}
};
class Solution {
public:
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(), [](const auto& u, const auto& v) {
return u[2] < v[2];
});
UnionFind uf(n);
vector<vector<int>> ans(2);
vector<int> label(m);
for (int i = 0; i < m;) {
// 找出所有权值为 w 的边,下标范围为 [i, j)
int w = edges[i][2];
int j = i;
while (j < m && edges[j][2] == edges[i][2]) {
++j;
}
// 存储每个连通分量在图 G 中的编号
unordered_map<int, int> compToId;
// 图 G 的节点数
int gn = 0;
for (int k = i; k < j; ++k) {
int x = uf.findset(edges[k][0]);
int y = uf.findset(edges[k][1]);
if (x != y) {
if (!compToId.count(x)) {
compToId[x] = gn++;
}
if (!compToId.count(y)) {
compToId[y] = gn++;
}
}
else {
// 将自环边标记为 -1
label[edges[k][3]] = -1;
}
}
// 图 G 的边
vector<vector<int>> gm(gn), gmid(gn);
for (int k = i; k < j; ++k) {
int x = uf.findset(edges[k][0]);
int y = uf.findset(edges[k][1]);
if (x != y) {
int idx = compToId[x], idy = compToId[y];
gm[idx].push_back(idy);
gmid[idx].push_back(edges[k][3]);
gm[idy].push_back(idx);
gmid[idy].push_back(edges[k][3]);
}
}
vector<int> bridges = TarjanSCC(gn, gm, gmid).getCuttingEdge();
// 将桥边(关键边)标记为 1
for (int id: bridges) {
ans[0].push_back(id);
label[id] = 1;
}
for (int k = i; k < j; ++k) {
uf.unite(edges[k][0], edges[k][1]);
}
i = j;
}
// 未标记的边即为非桥边(伪关键边)
for (int i = 0; i < m; ++i) {
if (!label[i]) {
ans[1].push_back(i);
}
}
return ans;
}
};