Description

給定 n 個氣球,每個氣球上有一個數字。戳破氣球 i 可獲得 nums[i-1] * nums[i] * nums[i+1] 的硬幣,戳破後左右氣球變為相鄰。求能收集的最大硬幣數。

Example:

Input: nums = [3,1,5,8] Output: 167

Intuition#

逆向思考:不是決定先戳哪個,而是決定在區間 (i, j) 中「最後戳哪個」氣球 k。

  • 正向思考(先戳哪個)很難處理,因為戳破後陣列結構改變
  • 逆向思考:dp[i][j] 代表戳破 (i, j) 開區間內所有氣球能獲得的最大硬幣
  • 選擇 k 作為最後一個戳破的氣球,此時 k 的左右鄰居就是 i 和 j
  • 轉移:dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j])
  • 在 nums 前後各加一個 1,方便處理邊界

Approaches#

1: 記憶化搜尋(Top-Down)#

  • 概念: 遞迴 + memo,枚舉區間內最後被戳破的氣球。
  • 時間複雜度: O(n^3)
  • 空間複雜度: O(n^2)
class Solution {
    fun maxCoins(nums: IntArray): Int {
        val n = nums.size
        val extended = IntArray(n + 2)
        extended[0] = 1
        extended[n + 1] = 1
        for (i in nums.indices) extended[i + 1] = nums[i]
        val memo = Array(n + 2) { IntArray(n + 2) { -1 } }
        return dp(extended, memo, 0, n + 1)
    }

    private fun dp(nums: IntArray, memo: Array<IntArray>, left: Int, right: Int): Int {
        if (left + 1 == right) return 0
        if (memo[left][right] != -1) return memo[left][right]
        var result = 0
        for (k in left + 1 until right) {
            result = maxOf(
                result,
                dp(nums, memo, left, k) + dp(nums, memo, k, right) +
                    nums[left] * nums[k] * nums[right]
            )
        }
        memo[left][right] = result
        return result
    }
}

⭐2: 區間 DP(Bottom-Up)#

  • 概念: 按區間長度從小到大填表,dp[i][j] 為開區間 (i, j) 的最大硬幣數。
  • 時間複雜度: O(n^3)
  • 空間複雜度: O(n^2)
class Solution {
    fun maxCoins(nums: IntArray): Int {
        val n = nums.size
        val extended = IntArray(n + 2)
        extended[0] = 1
        extended[n + 1] = 1
        for (i in nums.indices) extended[i + 1] = nums[i]

        val dp = Array(n + 2) { IntArray(n + 2) }
        for (len in 2..n + 1) {
            for (i in 0..n + 1 - len) {
                val j = i + len
                for (k in i + 1 until j) {
                    dp[i][j] = maxOf(
                        dp[i][j],
                        dp[i][k] + dp[k][j] + extended[i] * extended[k] * extended[j]
                    )
                }
            }
        }
        return dp[0][n + 1]
    }
}

🔑 Takeaways#

  • Pattern: 區間 DP — 枚舉區間內「最後操作」的元素
  • 關鍵技巧: 當「先做什麼」很難分析時,試著逆向思考「最後做什麼」;在陣列兩端加哨兵值簡化邊界處理