1. 程式人生 > >Leetcode 494:目標和(最詳細的解法!!!)

Leetcode 494:目標和(最詳細的解法!!!)

給定一個非負整數陣列,a1, a2, …, an, 和一個目標數,S。現在你有兩個符號 +-。對於陣列中的任意一個整數,你都可以從 +-中選擇一個符號新增在前面。

返回可以使最終陣列和為目標數 S 的所有新增符號的方法數。

示例 1:

輸入: nums: [1, 1, 1, 1, 1], S: 3
輸出: 5
解釋: 

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5種方法讓最終目標和為3。

注意:

  1. 陣列的長度不會超過20,並且陣列中的值全為正數。
  2. 初始的陣列的和不會超過1000。
  3. 保證返回的最終結果為32位整數。

解題思路

這個問題非常簡單,我們首先想到的是通過回溯法解決這個問題。我們定義函式f(i, target)表示[0:i]這個區間內目標為target的方法數,那麼我們很容易得到下面這個表示式

  • f(i, target)=f(i-1, target-nums[i])+f(i-1, target+nums[i])

邊界條件就是i==len(nums)時,我們要判斷target == 0,如果是的話返回1,否則0

class Solution:
    def findTargetSumWays(self, nums, S):
        """
        :type nums: List[int]
        :type S: int
        :rtype: int
        """
return self._findTargetSumWays(nums, 0, S) def _findTargetSumWays(self, nums, index, target): if index == len(nums): if target == 0: return 1 return 0 return self._findTargetSumWays(nums, index + 1, target-nums[index])\ +
self._findTargetSumWays(nums, index + 1, target+nums[index])

同樣對於這種問題我們都可以通過記憶化搜尋的方式去解決。這裡要注意的細節就是,我們要記憶兩種狀態分別是index & target

class Solution:
    def findTargetSumWays(self, nums, S):
        """
        :type nums: List[int]
        :type S: int
        :rtype: int
        """
        mem = dict()
        return self._findTargetSumWays(nums, 0, S, mem)

    def _findTargetSumWays(self, nums, index, target, mem):
        if index == len(nums):
            if target == 0:
                return 1
            return 0

        tmp = "{},{}".format(index, target)
        if tmp in mem:
            return mem[tmp]

        mem[tmp] = self._findTargetSumWays(nums, index + 1, target-nums[index], mem)\
                    + self._findTargetSumWays(nums, index + 1, target+nums[index], mem)

        return mem[tmp]

這個問題最簡潔的思路時通過動態規劃來解。這實際上是一個揹包問題,在揹包問題中,我們要考慮物品放還是不放,而在這個問題中我們要考慮是加上一個數還是減去一個數。此時的揹包的大小應該可以容納[-sum(nums),sum(nums)]這個區間的所有數,而我們有len(nums)個元素,所以我們最後需要一個(len(nums)+1)(2*sum_nums + 1)大小的陣列用來儲存狀態。傳統揹包問題可以用一個數組解決啊?著我們後面再說怎麼用一維陣列解決這個問題。接下來的過程很清晰,無非就是加上還是減去一個數的問題

  • mem[i][j] = mem[i-1][j+nums[i-1]] while j + nums[i-1] < 2*sum_nums + 1
  • mem[i][j] = mem[i-1][j-nums[i-1]] while j - nums[i-1] >= 0

最終程式碼如下

class Solution:
    def findTargetSumWays(self, nums, S):
        """
        :type nums: List[int]
        :type S: int
        :rtype: int
        """
        sum_nums = sum(nums)
        if sum_nums < S or -sum_nums > S:
            return 0
        
        len_nums = len(nums)
        mem = [[0]*(2*sum_nums + 1) for _ in range(len_nums+1)]
        mem[0][sum_nums] = 1
        for i in range(1, len_nums + 1):
            for j in range(2*sum_nums + 1):
                if j + nums[i - 1] < 2*sum_nums + 1:
                    mem[i][j] += mem[i - 1][j + nums[i - 1]]
                if j - nums[i - 1] >= 0:
                    mem[i][j] += mem[i - 1][j - nums[i - 1]]
                    
        return mem[len_nums][sum_nums + S]      

我們怎麼通過一維陣列解決這個問題呢?實際上這個問題是之前Leetcode 416:分割等和子集(最詳細的解法!!!) 提高,我們這裡的問題同樣可以理解為將nums拆分為P&N兩個子集(P做加法,N做減法),那麼我們的問題就變成了sum(P)-sum(N)=target也就是2*sum(P)=target+sum(nums),也就是說target+sum(nums)必須是一個偶數,這是一個非常重要的結論。我們通過這種思想結合動態規劃,就可以寫出下面的程式碼

class Solution:
    def findTargetSumWays(self, nums, S):
        """
        :type nums: List[int]
        :type S: int
        :rtype: int
        """
        sum_nums = sum(nums)
        if sum_nums < S or (S + sum_nums)%2 != 0:
            return 0

        target = (S + sum_nums) >> 1
        mem = [0]*(target + 1)
        mem[0] = 1
        for num in nums:
            for i in range(target, num-1, -1):
                mem[i] += mem[i - num]
        return mem[target]

數學與程式的完美融合,這也是很多黑客喜歡的程式設計方式。

reference:

如有問題,希望大家指出!!!