代码链接:https://leetcode.cn/problems/target-sum/solution/by-flix-rkb5/
题目分析:
记数组的元素和为 total,添加
+
+
+ 号的元素之和为 pos,添加 - 号的元素之和为 neg,则有以下关系:
{
pos
+
n
e
g
=
total
pos
−
n
e
g
=
target
\left\{\begin{array}{l} \text { pos }+n e g=\text { total } \\ \text { pos }-n e g=\text { target } \end{array}\right.
{ pos +neg= total pos −neg= target
进一步可得:
{
pos
=
(
total
+
target
)
/
2
neg
=
(
total
−
target
)
/
2
\left\{\begin{array}{l} \text { pos }=(\text { total }+\text { target }) / 2 \\ \text { neg }=(\text { total }-\text { target }) / 2 \end{array}\right.
{ pos =( total + target )/2 neg =( total − target )/2
问题转化:
此时不难发现,本题实质上是一道「0-1 背包问题」:给定一个只包含正整数的非空数组 nums,判断是否可以从数组中选出一些数字(每个元素最多选一次),使得选出的这些数字的和刚好等于 pos 或者 neg。
程序执行前可先判断 n u m s nums nums 是否满足一些基本条件,如 t o t a l > t a r g e t total>target total>target、 t o t a l + t a r g e t total+target total+target能被 2 整除等,若不满足程序则可直接返回 0。
01背包问题
动态规划是解决「0-1 背包问题」的标准做法。一般地,我们定义: d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 件物品放入一个容量为 j j j 的背包可以获得的最大价值,则状态转移过程可表示为:
- 不选择第 i i i 件物品:问题转化为了前 i − 1 i-1 i−1 件物品放入容量为 j j j 的背包中所获得的价值: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i−1][j] ;
- 选择第
i
i
i 件物品:第
i
i
i 件物品占据容量
w
i
w_i
wi,前
i
−
1
i-1
i−1 件物品放入剩下的容量为
j
−
w
i
j-w_i
j−wi 的背包中,问题也就转化为了前
i
−
1
i-1
i−1 件物品放入容量为
j
−
w
i
j-w_i
j−wi 的背包中所获得的价值
d
p
[
i
−
1
]
[
j
−
w
i
]
dp[i-1][j-w_i]
dp[i−1][j−wi] 加上要放入的第
i
i
i 件物品的价值
v
i
v_i
vi
dp[i][j]=dp[i-1][j-w_i]+v_i
。注意,能放入第 i i i 件物品的前提为: w i ≤ j w_i \leq j wi≤j。
两种情况取较大者:
d
p
[
i
]
[
j
]
=
max
{
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
w
i
]
+
v
i
}
d p[i][j]=\max \left\{d p[i-1][j], d p[i-1]\left[j-w_i\right]+v_i\right\}
dp[i][j]=max{dp[i−1][j],dp[i−1][j−wi]+vi}
求最优解的背包问题中,有的题目要求 恰好装满背包 时的最优解,有的题目则要求 不超过背包容量 时的最优解。一种区别这两种问法的实现方法是在状态初始化的时候有所不同。[摘自@ 《背包问题九讲》 (网页版) (PDF版)]
初始化的 d p dp dp 数组事实上就是在背包中没有放入任何物品时的合法状态:
- 如果要求恰好装满背包,那么在初始化时 d p [ i ] [ 0 ] = 0 dp[i][0]=0 dp[i][0]=0,其它 d p [ i ] [ 1 , 2 , . . . , ∗ ] dp[i][1,2,...,∗] dp[i][1,2,...,∗] 均设为 − ∞ -\infty −∞。这是因为此时只有容量为 0 的背包可能被价值为 0 的 nothing “恰好装满”,而其它容量的背包均没有合法的解,属于未定义的状态。
- 如果只是要求不超过背包容量而使得背包中的物品价值尽量大,初始化时应将 d p [ ∗ ] [ ∗ ] dp[∗][∗] dp[∗][∗] 全部设为 0。这是因为对应于任何一个背包,都有一个合法解为 “什么都不装”,价值为 0。
本题题目分析:
对于本题而言, n u m s [ i ] nums[i] nums[i] 则对应于常规背包问题中第 i i i 件物品的重量。我们要做的是从数组 n u m s nums nums 中选出若干个数字(每个元素最多选一次)使得其和刚好等于 p o s pos pos 或者 n e g neg neg,并计算有多少种不同的选择方式。
I. 状态定义
对于本题,定义 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示:从前 i i i 个数字中选出若干个,使得被选出的数字其和为 j j j 的方案数目。
II. 状态转移
根据本题的要求,上述「0-1 背包问题」的状态转移方程(1)可修改为:
当 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n 时,对于数组 nums 中的第 i i i 个元素 num ( i i i 的计数从 1 开始),遍历 0 ≤ j ≤ n e g 0 \leq j \leq n e g 0≤j≤neg ,计算 d p [ i ] [ j ] d p[i][j] dp[i][j] 的值:
- 如果 j < n u m j<n u m j<num ,则不能选 num,此时有 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] d p[i][j]=d p[i-1][j] dp[i][j]=dp[i−1][j] ;
- 如果 j ≥ n u m j \geq n u m j≥num ,则如果不选 num,方案数是 d p [ i − 1 ] [ j ] d p[i-1][j] dp[i−1][j] ,如果选 n u m n u m num ,方案数是 d p [ i − 1 ] [ j − d p[i-1][j- dp[i−1][j− n u m ] n u m] num] ,此时有 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i − 1 ] [ j − n u m ] d p[i][j]=d p[i-1][j]+d p[i-1][j-n u m] dp[i][j]=dp[i−1][j]+dp[i−1][j−num] 。
因此状态转移方程如下:
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
]
,
j
<
n
u
m
s
[
i
]
d
p
[
i
−
1
]
[
j
]
+
d
p
[
i
−
1
]
[
j
−
n
u
m
s
[
i
]
]
,
j
≥
n
u
m
s
[
i
]
d p[i][j]= \begin{cases}d p[i-1][j], & j<n u m s[i] \\ d p[i-1][j]+d p[i-1][j-n u m s[i]], & j \geq n u m s[i]\end{cases}
dp[i][j]={dp[i−1][j],dp[i−1][j]+dp[i−1][j−nums[i]],j<nums[i]j≥nums[i]
最终得到 d p [ n ] [ n e g ] d p[n][neg] dp[n][neg] 的值即为答案。
III. 初始化
记数组 nums 的长度为 n n n。为便于状态更新,减少对边界的判断,初始二维 d p d p dp 数组维度为 ( n + 1 ) × ( ∗ ) (n+1) \times(*) (n+1)×(∗) ,其中第一维为 n + 1 n+1 n+1 也意味着:第 i i i 个数字为 n u m s [ i − 1 ] nums[i-1] nums[i−1],第 1 个数字为 n u m s [ 0 ] nums[0] nums[0],第 0 个数字为空。
初始化时:
- d p [ 0 ] [ 0 ] = 1 dp[0][0]=1 dp[0][0]=1:表示从前 0 个数字中选出若干个数字使得其和为 0 的方案数为 1 ,即「空集合」不选任何数字即可得到 0。
- 对于其他 d p [ 0 ] [ j ] , j ≥ 1 dp[0][j], j \geq 1 dp[0][j],j≥1 ,则有 d p [ 0 ] [ j ] = 0 d p[0][j]=0 dp[0][j]=0:「空集合」无法选出任何数字使得其和为 j ≥ 1 j \geq 1 j≥1 。 d p [ i ] [ 0 ] = 1 d p[i][0]=1 dp[i][0]=1 在程序迭代实现中已有体现,在此无需提前重复定义。
d p [ i ] [ 0 ] = 1 dp[i][0]=1 dp[i][0]=1 在程序迭代实现中已有体现,在此无需提前重复定义。
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total = sum(nums)
if abs(target) > total: # target可能为负
return 0
if (total + target) % 2 == 1: # 不能被2整除【对应于pos不是整数】
return 0
pos = (total + target) // 2
neg = (total - target) // 2
capcity = min(pos, neg) # 取pos和neg中的较小者,以使得dp空间最小
n = len(nums)
# 初始化
dp = [[0] * (capcity+1) for _ in range(n+1)]
# dp[i][j]: 从前i个元素中选出若干个其和为j的方案数
dp[0][0] = 1 # 其他 dp[0][j]均为0
# 状态更新
for i in range(1, n+1):
for j in range(capcity+1):
if j < nums[i-1]: # 容量有限,无法选择第i个数字nums[i-1]
dp[i][j] = dp[i-1][j]
else: # 可选择第i个数字nums[i-1],也可不选【两种方式之和】
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]
return dp[n][capcity]