Description

給定一組二維平面上的點 points,兩點之間的連接成本為曼哈頓距離 |xi - xj| + |yi - yj|。回傳使所有點連通的最小成本。

Example:

Input: points = [[0,0],[2,2],[3,10],[5,2],[7,0]] Output: 20

Intuition#

最小生成樹 (MST) 問題 – 用 Prim 或 Kruskal 找出連通所有點的最小成本邊集。

  • 所有點兩兩之間都有邊(完全圖),邊權為曼哈頓距離
  • 需要找最小生成樹
  • Prim 適合稠密圖(邊多),Kruskal 適合稀疏圖

Approaches#

1: Kruskal + Union-Find#

  • 概念: 將所有邊按權重排序,依序加入不會形成環的邊,直到加入 n-1 條邊
  • 時間複雜度: O(n^2 log n)(排序 n^2 條邊)
  • 空間複雜度: O(n^2)
class Solution {
    private lateinit var parent: IntArray
    private lateinit var rank: IntArray

    fun minCostConnectPoints(points: Array<IntArray>): Int {
        val n = points.size
        val edges = mutableListOf<IntArray>() // [cost, i, j]

        for (i in 0 until n) {
            for (j in i + 1 until n) {
                val cost = Math.abs(points[i][0] - points[j][0]) + Math.abs(points[i][1] - points[j][1])
                edges.add(intArrayOf(cost, i, j))
            }
        }

        edges.sortBy { it[0] }
        parent = IntArray(n) { it }
        rank = IntArray(n)

        var totalCost = 0
        var edgeCount = 0
        for ((cost, u, v) in edges) {
            if (union(u, v)) {
                totalCost += cost
                edgeCount++
                if (edgeCount == n - 1) break
            }
        }
        return totalCost
    }

    private fun find(x: Int): Int {
        if (parent[x] != x) parent[x] = find(parent[x])
        return parent[x]
    }

    private 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
    }
}

⭐2: Prim(優先佇列)#

  • 概念: 從任意點開始,每次選擇連接已選集合和未選集合的最小權重邊,貪心擴展 MST
  • 時間複雜度: O(n^2 log n)
  • 空間複雜度: O(n^2)
class Solution {
    fun minCostConnectPoints(points: Array<IntArray>): Int {
        val n = points.size
        val inMST = BooleanArray(n)
        // [cost, pointIndex]
        val pq = PriorityQueue<IntArray>(compareBy { it[0] })
        pq.add(intArrayOf(0, 0))
        var totalCost = 0
        var edgesUsed = 0

        while (edgesUsed < n) {
            val (cost, u) = pq.poll()
            if (inMST[u]) continue
            inMST[u] = true
            totalCost += cost
            edgesUsed++

            for (v in 0 until n) {
                if (!inMST[v]) {
                    val dist = Math.abs(points[u][0] - points[v][0]) + Math.abs(points[u][1] - points[v][1])
                    pq.add(intArrayOf(dist, v))
                }
            }
        }
        return totalCost
    }
}
補充解法:Prim(O(n^2) 無 PQ)
  • 概念: 對於完全圖,用陣列記錄每個點到 MST 的最小距離,每次線性掃描找最小值,省去 PQ 開銷
  • 時間複雜度: O(n^2)
  • 空間複雜度: O(n)
class Solution {
    fun minCostConnectPoints(points: Array<IntArray>): Int {
        val n = points.size
        val minDist = IntArray(n) { Int.MAX_VALUE }
        val inMST = BooleanArray(n)
        minDist[0] = 0
        var totalCost = 0

        repeat(n) {
            var u = -1
            for (v in 0 until n) {
                if (!inMST[v] && (u == -1 || minDist[v] < minDist[u])) u = v
            }
            inMST[u] = true
            totalCost += minDist[u]
            for (v in 0 until n) {
                if (!inMST[v]) {
                    val dist = Math.abs(points[u][0] - points[v][0]) + Math.abs(points[u][1] - points[v][1])
                    minDist[v] = minOf(minDist[v], dist)
                }
            }
        }
        return totalCost
    }
}

🔑 Takeaways#

  • Pattern: 最小生成樹 – Prim(從頂點擴展)vs Kruskal(按邊排序)
  • 關鍵技巧: 完全圖中 Prim O(n^2) 版本比 PQ 版本更高效;Kruskal 需要先生成所有 O(n^2) 條邊