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。
注意:
- 陣列的長度不會超過20,並且陣列中的值全為正數。
- 初始的陣列的和不會超過1000。
- 保證返回的最終結果為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:
如有問題,希望大家指出!!!