Description

給定一個只含正整數的非空陣列 nums,判斷能否將其分割為兩個子集,使得兩個子集的元素總和相等。

Example:

Input: nums = [1,5,11,5] Output: true

Intuition#

問題等價於:能否從陣列中選出若干元素,使其總和恰好等於全部總和的一半?

  • 如果總和是奇數,直接回傳 false
  • 轉化為 0/1 背包問題:目標容量 = sum / 2
  • dp[j] 表示是否能用部分元素湊出和為 j

Approaches#

1: 2D DP (Standard 0/1 Knapsack)#

  • 概念: dp[i][j] 表示前 i 個元素能否湊出和 j
  • 時間複雜度: O(n * target)
  • 空間複雜度: O(n * target)
class Solution {
    fun canPartition(nums: IntArray): Boolean {
        val sum = nums.sum()
        if (sum % 2 != 0) return false
        val target = sum / 2
        val n = nums.size
        val dp = Array(n + 1) { BooleanArray(target + 1) }
        for (i in 0..n) dp[i][0] = true
        for (i in 1..n) {
            for (j in 1..target) {
                dp[i][j] = dp[i - 1][j]
                if (j >= nums[i - 1]) {
                    dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]]
                }
            }
        }
        return dp[n][target]
    }
}

⭐2: 1D DP (Space Optimized 0/1 Knapsack)#

  • 概念: 壓縮到一維,從後往前遍歷避免重複選取同一元素
  • 時間複雜度: O(n * target)
  • 空間複雜度: O(target)
class Solution {
    fun canPartition(nums: IntArray): Boolean {
        val sum = nums.sum()
        if (sum % 2 != 0) return false
        val target = sum / 2
        val dp = BooleanArray(target + 1)
        dp[0] = true
        for (num in nums) {
            for (j in target downTo num) {
                dp[j] = dp[j] || dp[j - num]
            }
        }
        return dp[target]
    }
}

🔑 Takeaways#

  • Pattern: 0/1 背包 — 每個元素只能用一次,判斷能否達到目標值
  • 關鍵技巧: 一維 DP 必須從後往前遍歷(downTo),確保每個元素只被選一次;如果從前往後會變成完全背包