416. Partition Equal Subset Sum

416. Partition Equal Subset Sum

首先我們要確立目標,目標是給定一個陣列能不能分成兩個子集合,讓這兩個子集合的總和相等。

第一個要去想的地方比較簡單,既然兩個子集合要相等,那一個子集合一定是原陣列總和的一半,我們的目標就是要找到一個一個子集合,其總和是原陣列總和的一半

另外,有可能原陣列的元素總和是奇數:例如:[1, 2, 1, 1] 這樣的陣列總和是 5 ,一定不會有辦法分成兩個集合,所以可以先用這樣的方式去剪枝(把所有總和為奇數的直接排除掉)。

接著是要怎麼選擇,每一個元素我們都可以有「選」或是「不選」這兩種方式,如果的話,我們的目標就會減少當下的數值,如果不選的話,目標並不會改變,一直到如果我們選了最後一個數字,如果最後目標剛好等於 0 ,那就是我們透過選擇,找到了目標

這樣的關係可以用一個遞迴的方式來呈現。

遞迴

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        total = sum(nums)        
        if total % 2 != 0:
            return False        
        def helper(i, target):
            if i == len(nums) - 1:
                return target == 0
            else:
                return helper(i + 1, target - nums[i]) or helper(i + 1, target)

        return helper(0, total//2)

既然遞迴的方式已經寫好了,上面這個函式很明顯的存在著重疊的子問題,例如最後一個位置的「選」或「不選」,在前面幾個元素在做選擇時,會一直重複的計算,因此可以用記憶法,來記憶著已經做過的選擇。而每個位置的選擇或不選擇,可以透過當前位置與目標來決定。

自頂向下

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        total = sum(nums)        
        if total % 2 != 0:
            return False        
        memo = {}
        def helper(i, target):
            if (i, target) in memo:
                return memo[(i, target)]
            if i == len(nums) - 1:
                return target == 0
            else:
                memo[(i, target)] = helper(i + 1, target - nums[i]) or helper(i + 1, target)
                return memo[(i, target)]

        return helper(0, total//2)

自底向上

一直以來我都覺得自底向上比較難想到,這題也是不太好想。

原陣列的每個位置都會有選擇或是不選擇兩種,自底向上的想法是,如果我們知道第 i 的位置在選擇的時候,是不是可以達到某個比我們的目標還要小的數值。

文字敘述很抽象,換成一個數學的例子就是,現在最後的目標是 5 ,前一個位置如果已經可以達到目標 5 了,那我這個位置就不用選擇,如果我的當前值是 2 ,那在前一個位置如果可以達到 5 - 2 = 3 ,那我就要選擇當前位置的元素,這一串選擇就是 L20 在做的事情。

一開始的 base case 則是如果說一開始的目標就是 0 ,那不管有沒有選都是符合條件了。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:

        total = sum(nums)
        if total % 2 != 0:
            return False

        M = len(nums)
        N = total // 2
        dp = [[False] * (N + 1) for _ in range(M+1)]

        for i in range(M+1):
            dp[i][0] = True


        for i in range(1, M+1):
            for j in range(1, N+1):
                if j - nums[i - 1] < 0:
                    dp[i][j] = dp[i - 1][j]
                else:
                    dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i-1]]

        return dp[M][N]