// 并查集模板 classUnionFind { 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); } intfindset(int x){ return parent[x] == x ? x : parent[x] = findset(parent[x]); } boolunite(int x, int y){ x = findset(x); y = findset(y); if (x == y) { returnfalse; } if (size[x] < size[y]) { swap(x, y); } parent[y] = x; size[x] += size[y]; --setCount; returntrue; } boolconnected(int x, int y){ x = findset(x); y = findset(y); return x == y; } };
classSolution { 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(), [](constauto& u, constauto& v) { return u[2] < v[2]; });
// 计算 value UnionFind uf_std(n); int value = 0; for (int i = 0; i < m; ++i) { if (uf_std.unite(edges[i][0], edges[i][1])) { value += edges[i][2]; } }
vector<vector<int>> ans(2); for (int i = 0; i < m; ++i) { // 判断是否是关键边 UnionFind uf(n); int v = 0; for (int j = 0; j < m; ++j) { if (i != j && uf.unite(edges[j][0], edges[j][1])) { v += edges[j][2]; } } if (uf.setCount != 1 || (uf.setCount == 1 && v > value)) { ans[0].push_back(edges[i][3]); continue; }
// 判断是否是伪关键边 uf = UnionFind(n); uf.unite(edges[i][0], edges[i][1]); v = edges[i][2]; for (int j = 0; j < m; ++j) { if (i != j && uf.unite(edges[j][0], edges[j][1])) { v += edges[j][2]; } } if (v == value) { ans[1].push_back(edges[i][3]); } } return ans; } };
classSolution { public List<List<Integer>> findCriticalAndPseudoCriticalEdges(int n, int[][] edges) { intm= edges.length; int[][] newEdges = newint[m][4]; for (inti=0; i < m; ++i) { for (intj=0; j < 3; ++j) { newEdges[i][j] = edges[i][j]; } newEdges[i][3] = i; } Arrays.sort(newEdges, newComparator<int[]>() { publicintcompare(int[] u, int[] v) { return u[2] - v[2]; } });
// 计算 value UnionFindufStd=newUnionFind(n); intvalue=0; for (inti=0; i < m; ++i) { if (ufStd.unite(newEdges[i][0], newEdges[i][1])) { value += newEdges[i][2]; } }
List<List<Integer>> ans = newArrayList<List<Integer>>(); for (inti=0; i < 2; ++i) { ans.add(newArrayList<Integer>()); } for (inti=0; i < m; ++i) { // 判断是否是关键边 UnionFinduf=newUnionFind(n); intv=0; for (intj=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 = newUnionFind(n); uf.unite(newEdges[i][0], newEdges[i][1]); v = newEdges[i][2]; for (intj=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; } }
// 并查集模板 classUnionFind { int[] parent; int[] size; int n; // 当前连通分量数目 int setCount;
publicUnionFind(int n) { this.n = n; this.setCount = n; this.parent = newint[n]; this.size = newint[n]; Arrays.fill(size, 1); for (inti=0; i < n; ++i) { parent[i] = i; } } publicintfindset(int x) { return parent[x] == x ? x : (parent[x] = findset(parent[x])); } publicbooleanunite(int x, int y) { x = findset(x); y = findset(y); if (x == y) { returnfalse; } if (size[x] < size[y]) { inttemp= x; x = y; y = temp; } parent[y] = x; size[x] += size[y]; --setCount; returntrue; } publicbooleanconnected(int x, int y) { x = findset(x); y = findset(y); return x == y; } }
# 并查集模板 classUnionFind: def__init__(self, n: int): self.parent = list(range(n)) self.size = [1] * n self.n = n # 当前连通分量数目 self.setCount = n deffindset(self, x: int) -> int: if self.parent[x] == x: return x self.parent[x] = self.findset(self.parent[x]) return self.parent[x] defunite(self, x: int, y: int) -> bool: x, y = self.findset(x), self.findset(y) if x == y: returnFalse if self.size[x] < self.size[y]: x, y = y, x self.parent[y] = x self.size[x] += self.size[y] self.setCount -= 1 returnTrue defconnected(self, x: int, y: int) -> bool: x, y = self.findset(x), self.findset(y) return x == y
classSolution: deffindCriticalAndPseudoCriticalEdges(self, n: int, edges: List[List[int]]) -> List[List[int]]: m = len(edges) for i, edge inenumerate(edges): edge.append(i) edges.sort(key=lambda x: x[2])
# 计算 value uf_std = UnionFind(n) value = 0 for i inrange(m): if uf_std.unite(edges[i][0], edges[i][1]): value += edges[i][2]
ans = [list(), list()] for i inrange(m): # 判断是否是关键边 uf = UnionFind(n) v = 0 for j inrange(m): if i != j and uf.unite(edges[j][0], edges[j][1]): v += edges[j][2] if uf.setCount != 1or (uf.setCount == 1and v > value): ans[0].append(edges[i][3]) continue
# 判断是否是伪关键边 uf = UnionFind(n) uf.unite(edges[i][0], edges[i][1]) v = edges[i][2] for j inrange(m): if i != j and uf.unite(edges[j][0], edges[j][1]): v += edges[j][2] if v == value: ans[1].append(edges[i][3]) return ans
funcnewUnionFind(n int) *unionFind { parent := make([]int, n) size := make([]int, n) for i := range parent { parent[i] = i size[i] = 1 } return &unionFind{parent, size, n} }
func(uf *unionFind) find(x int) int { if uf.parent[x] != x { uf.parent[x] = uf.find(uf.parent[x]) } return uf.parent[x] }
func(uf *unionFind) union(x, y int) bool { fx, fy := uf.find(x), uf.find(y) if fx == fy { returnfalse } if uf.size[fx] < uf.size[fy] { fx, fy = fy, fx } uf.size[fx] += uf.size[fy] uf.parent[fy] = fx uf.setCount-- returntrue }
funcfindCriticalAndPseudoCriticalEdges(n int, edges [][]int) [][]int { for i, e := range edges { edges[i] = append(e, i) } sort.Slice(edges, func(i, j int)bool { return edges[i][2] < edges[j][2] })
calcMST := func(uf *unionFind, ignoreID int) (mstValue int) { for i, e := range edges { if i != ignoreID && uf.union(e[0], e[1]) { mstValue += e[2] } } if uf.setCount > 1 { return math.MaxInt64 } return }
mstValue := calcMST(newUnionFind(n), -1)
var keyEdges, pseudokeyEdges []int for i, e := range edges { // 是否为关键边 if calcMST(newUnionFind(n), i) > mstValue { keyEdges = append(keyEdges, e[3]) continue }
var findCriticalAndPseudoCriticalEdges = function(n, edges) { const m = edges.length; for (const [i, edge] of edges.entries()) { edge.push(i); } edges.sort((a, b) => a[2] - b[2]);
// 计算 value const uf_std = newUnionFind(n); let value = 0; for (let i = 0; i < m; i++) { if (uf_std.unite(edges[i][0], edges[i][1])) { value += edges[i][2]; } }
const ans = [[], []];
for (let i = 0; i < m; i++) { // 判断是否是关键边 let uf = newUnionFind(n); let v = 0; for (let j = 0; j < m; j++) { if (i !== j && uf.unite(edges[j][0], edges[j][1])) { v += edges[j][2]; } } if (uf.setCount !== 1 || (uf.setCount === 1 && v > value)) { ans[0].push(edges[i][3]); continue; }
// 判断是否是伪关键边 uf = newUnionFind(n); uf.unite(edges[i][0], edges[i][1]); v = edges[i][2]; for (let j = 0; j < m; j++) { if (i !== j && uf.unite(edges[j][0], edges[j][1])) { v += edges[j][2]; } } if (v === value) { ans[1].push(edges[i][3]); } } return ans; };
在 Kruskal 算法中,对于任意的实数 w,只要我们将给定的边按照权值从小到大进行排序,那么当我们按照顺序处理完所有权值小于等于 w 的边之后,对应的并查集的连通性是唯一确定的,无论我们在排序时如何规定权值相同的边的顺序。
并且读者需要掌握:
给定一个无向图,使用 Tarjan 算法求出所有的桥边。
思路与算法
假设我们已经处理完了所有权值小于 w 的边,并查集的状态记为 U,该状态是唯一确定的。此时,我们同时处理所有权值等于 w 的边,记这些边的集合为 {e_w\。我们将 U 中的每一个连通分量看成一个节点,对于 {e_w\ 中的每一条无向边的两个端点,将它们在 U 中属于的连通分量对应的节点之间连接一条无向边,以此得到图 G。图 G 中会有三种类型的边:
自环边:即从一个节点指向本身的一条边。如果 {e_w\ 中的一条边的两个端点属于同一个连通分量,那么它在图 G 中表现为一条自环边。根据 Kruskal 算法,这样的边不会被添加进最小生成树中。
对于剩余的边,它们的两个端点属于不同的联通分量。如果我们将其作为 Kruskal 算法中第一条权值为 w 的边进行处理,那么这条边一定会被添加进最小生成树中。因此剩余的边要么是关键边,要么是伪关键边,它们在图 G 中的表现形式不同:
桥边。如果 {e_w\ 中的一条边对应了图 G 中的一条桥边,那么当这条边被删去时,图 G 的连通性就会发生改变。
这样的例子可能会帮助理解:如果我们将这条边作为 Kruskal 算法中最后一条权值为 w 的边进行处理,那么这条边还是会被添加进最小生成树中。
也就是说,这条边对于最小生成树而言是必须的,那么它就是关键边;
非桥边。如果 {e_w\ 中的一条边对应了图 G 中的一条非桥边,那么当这条边被删去时,图 G 的连通性不会发生改变。
这样的例子可能会帮助理解:如果我们将这条边作为 Kruskal 算法中最后一条权值为 w 的边进行处理,那么在此之前,并查集的连通性已经和(任意顺序)处理完所有权值为 w 的边之后的连通性一致,这条边就不会被添加进最小生成树中。
也就是说,这条边对于最小生成树而言不是必须的,那么它就是伪关键边。
因此图 G 中的桥边与 {e_w\ 中的关键边一一对应,非桥边(且非自环边)与 {e_w\ 中的非关键边一一对应。
我们可以使用 Tarjan 算法求出图 G 中的所有桥边,那么算法的时间复杂度是多少呢?如果图 G 中有 n_0 个节点和 m_0 条边,那么 Tarjan 算法的时间复杂度为 O(n_0 + m_0)。对于每一个 w 值对应的 {e_w\,我们并不需要将并查集中的每一个连通分量都作为一个节点放入图 G 中:即如果 {e_w\ 中包含 m_0 条边,那么它们最多会只连接了 2m_0 个连通分量,因此图 G 中最多有 2m_0 个节点和 m_0 条边(如果一条边是自环边,那么也不需要将其放入图 G 中),时间复杂度为 O(2m_0 + m_0) = O(m_0),与 {e_w\ 中包含的边数成正比。我们对所有的 w 值都需要进行一次 Tarjan 算法,这部分的总时间复杂度是 O(m)。对于排序的部分,时间复杂度是 O(m \log m),对于并查集的部分,时间复杂度是 O(m \cdot \alpha(n)),其中 \alpha 是阿克曼函数的反函数。三者中排序的时间复杂度在渐进意义下最大,因此总时间复杂度为 O(m \log m)。
private: voidgetCuttingEdge_(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); } } elseif (id != parentEdgeId) { low[u] = min(low[u], dfn[v]); } } }
classSolution { 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(), [](constauto& u, constauto& 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]); } }
classSolution: deffindCriticalAndPseudoCriticalEdges(self, n: int, edges: List[List[int]]) -> List[List[int]]: m = len(edges) for i, edge inenumerate(edges): edge.append(i) edges.sort(key=lambda x: x[2])
uf = UnionFind(n) ans0 = list() label = [0] * m
i = 0 while i < m: # 找出所有权值为 w 的边,下标范围为 [i, j) w = edges[i][2] j = i while j < m and edges[j][2] == edges[i][2]: j += 1
# 存储每个连通分量在图 G 中的编号 compToId = dict() # 图 G 的节点数 gn = 0 for k inrange(i, j): x = uf.findset(edges[k][0]) y = uf.findset(edges[k][1]) if x != y: if x notin compToId: compToId[x] = gn gn += 1 if y notin compToId: compToId[y] = gn gn += 1 else: # 将自环边标记为 -1 label[edges[k][3]] = -1 # 图 G 的边 gm = collections.defaultdict(list) gmid = collections.defaultdict(list) for k inrange(i, j): x = uf.findset(edges[k][0]) y = uf.findset(edges[k][1]) if x != y: idx, idy = compToId[x], compToId[y] gm[idx].append(idy) gmid[idx].append(edges[k][3]) gm[idy].append(idx) gmid[idy].append(edges[k][3])
bridges = TarjanSCC(gn, gm, gmid).getCuttingEdge() # 将桥边(关键边)标记为 1 ans0.extend(bridges) for iden in bridges: label[iden] = 1
for k inrange(i, j): uf.unite(edges[k][0], edges[k][1]) i = j
# 未标记的边即为非桥边(伪关键边) ans1 = [i for i inrange(m) if label[i] == 0]