Hard草稿★★★★★O(n * α(n)) 時間 · O(n) 空間
GraphUnion-FindTreeSorting
Patterns🔗 並查集🕸️ 圖遍歷・連通分量🔢 排序後處理
尚未複習過

解法已隱藏 — 先讀題目敘述、自己想想看,再點上方按鈕揭曉。

2421Number of Good PathsAdvanced GraphsHardAdvanced Graphs

給定一棵有 n 個節點的樹,每個節點有值 vals[i]。一條「好路徑」的兩端節點值相同,且路徑上所有節點值都不大於端點值。求好路徑的數量(包括長度為 0 的路徑,即每個節點本身)。

Example:

Input: vals = [1,3,2,1,3], edges = [[0,1],[0,2],[2,3],[2,4]] Output: 6

Intuition

TIP

按節點值從小到大加入,用 Union-Find 合併。每次加入一批相同值的節點時,計算新形成的好路徑數。

Approaches

⭐ 1. Union-Find + Sort — O(n * α(n)) / O(n)
  • Idea: 按節點值排序,依序將節點及其邊加入 Union-Find。每組相同值的節點中,同一連通分量內的 k 個節點貢獻 C(k,2) 條好路徑。
  • Time: O(n * α(n))(近似線性)
  • Space: O(n)
class Solution {
    private lateinit var parent: IntArray
    private lateinit var rank: IntArray

    fun numberOfGoodPaths(vals: IntArray, edges: Array<IntArray>): Int {
        val n = vals.size
        parent = IntArray(n) { it }
        rank = IntArray(n)

        val graph = Array(n) { mutableListOf<Int>() }
        for ((u, v) in edges) {
            graph[u].add(v)
            graph[v].add(u)
        }

        // 按值分組
        val valueToNodes = TreeMap<Int, MutableList<Int>>()
        for (i in 0 until n) {
            valueToNodes.getOrPut(vals[i]) { mutableListOf() }.add(i)
        }

        var result = n  // 每個節點本身是一條長度 0 的好路徑

        for ((value, nodes) in valueToNodes) {
            // 將當前值的節點與值 <= value 的鄰居合併
            for (node in nodes) {
                for (neighbor in graph[node]) {
                    if (vals[neighbor] <= value) {
                        union(node, neighbor)
                    }
                }
            }

            // 計算同一連通分量中相同值的節點對數
            val countByRoot = HashMap<Int, Int>()
            for (node in nodes) {
                val root = find(node)
                countByRoot[root] = (countByRoot[root] ?: 0) + 1
            }

            for (count in countByRoot.values) {
                result += count * (count - 1) / 2
            }
        }

        return result
    }

    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) {
        val px = find(x)
        val py = find(y)
        if (px == py) return
        if (rank[px] < rank[py]) parent[px] = py
        else if (rank[px] > rank[py]) parent[py] = px
        else { parent[py] = px; rank[px]++ }
    }
}
2. Edge-Sorted Union-Find — O(n * α(n)) / O(n)
  • Idea: 將邊按兩端最大值排序,依序加入。處理到值 v 的邊時,統計值為 v 的節點在同一分量中的數量。
  • Time: O(n * α(n))
  • Space: O(n)
class Solution {
    private lateinit var parent: IntArray
    private lateinit var rank: IntArray

    fun numberOfGoodPaths(vals: IntArray, edges: Array<IntArray>): Int {
        val n = vals.size
        parent = IntArray(n) { it }
        rank = IntArray(n)

        // 按邊的兩端最大值排序
        edges.sortBy { maxOf(vals[it[0]], vals[it[1]]) }

        // 每個分量中最大值的計數
        val maxCount = IntArray(n) { 1 }  // 每個節點初始為自己分量的最大值

        var result = n

        for ((u, v) in edges) {
            val pu = find(u)
            val pv = find(v)
            if (pu == pv) continue

            val maxU = vals[pu]
            val maxV = vals[pv]

            if (maxU == maxV) {
                result += maxCount[pu] * maxCount[pv]
                union(pu, pv)
                val root = find(pu)
                maxCount[root] = maxCount[pu] + maxCount[pv]
            } else if (maxU > maxV) {
                union(pv, pu)
                val root = find(pu)
                maxCount[root] = maxCount[pu]
            } else {
                union(pu, pv)
                val root = find(pv)
                maxCount[root] = maxCount[pv]
            }
        }

        return result
    }

    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) {
        val px = find(x)
        val py = find(y)
        if (px == py) return
        if (rank[px] < rank[py]) parent[px] = py
        else if (rank[px] > rank[py]) parent[py] = px
        else { parent[py] = px; rank[px]++ }
    }
}

🔑 Takeaways