Description
給定一個加權無向圖,找出所有最小生成樹(MST)中的關鍵邊和偽關鍵邊。關鍵邊是移除後會使 MST 權重增加的邊;偽關鍵邊是可以出現在某些 MST 中但不是所有 MST 都需要的邊。
Example:
Input: n = 5, edges = [[0,1,1],[1,2,1],[2,3,2],[0,3,2],[0,4,3],[3,4,3],[1,4,6]] Output: [[0,1],[2,3,4,5]]
Intuition#
對每條邊測試:排除後 MST 增加 -> 關鍵邊;強制包含後 MST 不變 -> 偽關鍵邊。
- 先用 Kruskal 求出 MST 的基準權重
- 對每條邊:
- 排除該邊後重新求 MST,若權重增加或無法形成 MST -> 關鍵邊
- 非關鍵邊,強制包含該邊後求 MST,若權重等於基準 -> 偽關鍵邊
Approaches#
⭐1: Kruskal + 逐邊測試#
- 概念: 基準 MST + 對每條邊做排除/強制測試
- 時間複雜度:
O(E^2 * α(V)) - 空間複雜度:
O(V + E)
class Solution {
fun findCriticalAndPseudoCriticalEdges(n: Int, edges: Array<IntArray>): List<List<Int>> {
// 記錄原始索引
val indexed = Array(edges.size) { i -> intArrayOf(edges[i][0], edges[i][1], edges[i][2], i) }
indexed.sortBy { it[2] }
val baseMST = kruskal(n, indexed, -1, -1)
val critical = mutableListOf<Int>()
val pseudo = mutableListOf<Int>()
for (i in indexed.indices) {
// 排除邊 i
val without = kruskal(n, indexed, i, -1)
if (without > baseMST) {
critical.add(indexed[i][3])
} else {
// 強制包含邊 i
val with = kruskal(n, indexed, -1, i)
if (with == baseMST) {
pseudo.add(indexed[i][3])
}
}
}
return listOf(critical, pseudo)
}
private fun kruskal(n: Int, edges: Array<IntArray>, exclude: Int, include: Int): Int {
val parent = IntArray(n) { it }
val rank = IntArray(n)
var weight = 0
var edgeCount = 0
fun find(x: Int): Int {
if (parent[x] != x) parent[x] = find(parent[x])
return parent[x]
}
fun union(x: Int, y: Int): Boolean {
val px = find(x)
val py = find(y)
if (px == py) return false
if (rank[px] < rank[py]) parent[px] = py
else if (rank[px] > rank[py]) parent[py] = px
else { parent[py] = px; rank[px]++ }
return true
}
// 強制包含
if (include != -1) {
val (u, v, w) = edges[include]
union(u, v)
weight += w
edgeCount++
}
for (i in edges.indices) {
if (i == exclude || i == include) continue
val (u, v, w) = edges[i]
if (union(u, v)) {
weight += w
edgeCount++
}
}
return if (edgeCount == n - 1) weight else Int.MAX_VALUE
}
}2: 簡化版(相同邏輯)#
- 概念: 將 Union-Find 封裝為類別,程式碼更清晰
- 時間複雜度:
O(E^2 * α(V)) - 空間複雜度:
O(V + E)
class Solution {
fun findCriticalAndPseudoCriticalEdges(n: Int, edges: Array<IntArray>): List<List<Int>> {
val m = edges.size
val idx = (0 until m).sortedBy { edges[it][2] }
fun mst(skip: Int, pick: Int): Int {
val uf = IntArray(n) { it }
var w = 0; var cnt = 0
fun find(x: Int): Int { var a = x; while (uf[a] != a) { uf[a] = uf[uf[a]]; a = uf[a] }; return a }
fun unite(x: Int, y: Int): Boolean { val a = find(x); val b = find(y); if (a == b) return false; uf[a] = b; return true }
if (pick >= 0) { unite(edges[pick][0], edges[pick][1]); w += edges[pick][2]; cnt++ }
for (i in idx) {
if (i == skip) continue
if (unite(edges[i][0], edges[i][1])) { w += edges[i][2]; cnt++ }
}
return if (cnt == n - 1) w else Int.MAX_VALUE
}
val base = mst(-1, -1)
val critical = mutableListOf<Int>()
val pseudo = mutableListOf<Int>()
for (i in 0 until m) {
if (mst(i, -1) > base) critical.add(i)
else if (mst(-1, i) == base) pseudo.add(i)
}
return listOf(critical, pseudo)
}
}🔑 Takeaways#
- Pattern: MST 邊分類 – 排除/強制測試法
- 關鍵技巧: 排除一條邊後 MST 權重增加或不連通 -> 關鍵邊;非關鍵邊中強制包含後 MST 權重不變 -> 偽關鍵邊;時間複雜度 O(E^2) 在 E 較小時可接受