1494 目标和转 0-1 背包
21. 前提条件
3
4每个数有两种选择:
5加正号:选到正号集合 P
6加负号:选到负号集合 N
7
8设:
9
10所有数总和 = s
11正号集合的和 = p
12负号集合的和 = s - p
13
14题目要求:正号和 - 负号和 = target
15
16所以:
17p - (s - p) = target
18
19展开:
202p - s = target
21
22所以:
23p = (s + target) / 2
24也就是转化成两个值不同情况的取舍问题
252. 什么情况下无解?
26
27因为:
28p = (s + target) / 2
29
30所以必须满足:
31s + target >= 0
32
33并且:
34
35(s + target) % 2 == 0
36
37更完整地说:
38
39如果 target > s 或 target < -s,也无解。
40
41例如:
42
43nums = [1,2,3]
44s = 6
45target = 10
46
47最大也只能得到 6,不可能得到 10。
48
494. 为什么变成 0-1 背包?
50
51原题是:每个数前面选 + 或 -
52
53等价于:从 nums 里选一些数放进正号集合 P
54
55要求:
56
57选出来的数的和 = p
58
59其中:
60
61p = (s + target) / 2
62
63所以变成:从数组中选一些数,使它们的和等于 p,问有多少种选法。
64
65这就是 0-1 背包的计数版本的转化理解。
66
675. 和普通 0-1 背包的区别
68
69普通 0-1 背包常见问法:容量不超过 capacity 时,最大价值是多少?
70
71所以它是:最大值问题
72
73转移用:max(...)
74
75目标和这题问的是:
76
77有多少种选法能刚好凑出 p?
78
79所以它是:方案数问题
80
81转移用:+
826. dp[j] 怎么理解?
83
84定义:dp[j] 表示:从已经看过的数字里,凑出和 j 的方案数。
85
86初始化:
87
88dp[0] = 1;
89
90意思是:什么都不选,凑出 0,有 1 种方案。
91
92这点非常重要。
93
947. 转移怎么理解?
95
96对于当前数:
97
98x = nums[i]
99
100要凑出 j,有两类方案:
101
1021. 不选 x:
103 原来 dp[j] 里的方案还在。
104
1052. 选 x:
106 那么之前必须先凑出 j - x。
107 这些方案数量是 dp[j - x]。
108
109所以dp[j] = dp[j] + dp[j - x];
110
111也就是:
112
113dp[j] += dp[j - x];
114
115这就是“方案数相加”。
116
1178. 为什么要倒序?
118
119因为每个元素只能用一次。
120
121for (int j = capacity; j >= x; j--) {
122 dp[j] += dp[j - x];
123}
124
125倒序可以保证:
126
127dp[j - x] 还是上一轮的结果,
128不会让当前 nums[i] 被重复使用。
129
130如果正序,就会变成完全背包,当前元素可能被用多次。
131也就是说本题把正负号两个情况转换成了01背包,正号集合和 = p,负号集合和 = s-p
132则:p-(s-p)=target
133推出:
134p=(s+target)/2
135--------------------------------
136问题转化:
137从nums中选一些数
138使其和为capacity
139capacity=(s+target)/2
140这样之后Dp数组里面总是有两种情况选x和不选x,这样就不断往前回溯,倒序是为了保证每次更新
141dp[0]是默认什么都不选的情况,也就是空集,这种情况是默认为1,后续是选进来的情况加入集合,capacity作为选出符合要求dp的情况,其实默认就是选出正号集合的和
1429. C 语言作答
143#include <stdlib.h>
144
145int findTargetSumWays(int* nums, int numsSize, int target) {
146 int s = 0;
147
148 for (int i = 0; i < numsSize; i++) {
149 s += nums[i];
150 }
151
152 if (target > s || target < -s) {
153 return 0;
154 }
155
156 if ((s + target) % 2 != 0) {
157 return 0;
158 }
159
160 int capacity = (s + target) / 2;
161
162 int* dp = malloc((capacity + 1) * sizeof(int));
163
164 for (int j = 0; j <= capacity; j++) {
165 dp[j] = 0;
166 }
167
168 dp[0] = 1;
169
170 for (int i = 0; i < numsSize; i++) {
171 int x = nums[i];
172
173 for (int j = capacity; j >= x; j--) {
174 dp[j] += dp[j - x];
175 }
176 }
177
178 int ans = dp[capacity];
179
180 free(dp);
181
182 return ans;
183}
184二、拿py理解一下
1851. 先想递归
186class Solution:
187 def findTargetSumWays(self, nums: List[int], target: int) -> int:
188 s = sum(nums) - abs(target)
189 if s < 0 or s % 2:
190 return 0
191
192 @cache # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
193 def dfs(i: int, c: int) -> int:
194 if i < 0:
195 return 1 if c == 0 else 0
196 if c < nums[i]:
197 return dfs(i - 1, c) # 只能不选
198 return dfs(i - 1, c) + dfs(i - 1, c - nums[i]) # 不选 + 选
199
200 m = s // 2 # 背包容量
201 return dfs(len(nums) - 1, m)
202定义:dfs(i, sum)
203
204表示:
205
206处理到第 i 个数时,当前和是 sum,最后能凑成 target 的方案数。
207
208对于 nums[i] 有两个选择。
209
210选择一:加 +
211dfs(i + 1, sum + nums[i])
212选择二:加 -
213dfs(i + 1, sum - nums[i])
214
215所以:
216
217dfs(i, sum)= dfs(i + 1, sum + nums[i])+ dfs(i + 1, sum - nums[i])
218
219为什么是加?
220
221因为题目问:
222
223有多少种方案
224
225所以左右两种选择的方案数要加起来。
226
2273. 记忆化搜索:处理负数 sum
228
229问题是:sum 可能是负数,不能直接当数组下标。
230
231所以要加一个偏移量。
232
233设:total = nums 所有元素之和
234
235那么 sum 的范围是:
236
237[-total, total]
238
239可以用:
240
241sum + total
242
243把负数下标平移成非负数。
244
245例如:
246
247sum = -3
248total = 10
249index = sum + total = 7
2505. 记忆化搜索 C 代码
251
252这里用二维数组:
253
254memo[i][sum + total]
255
256表示:
257
258处理到 i,当前和为 sum 时的方案数。
259#include <stdlib.h>
260
261int dfs(int* nums, int numsSize, int target, int i, int sum,
262 int total, int** memo) {
263 if (i == numsSize) {
264 if (sum == target) {
265 return 1;
266 } else {
267 return 0;
268 }
269 }
270
271 int index = sum + total;
272
273 if (memo[i][index] != -1) {
274 return memo[i][index];
275 }
276
277 int add = dfs(nums, numsSize, target, i + 1, sum + nums[i], total, memo);
278 int sub = dfs(nums, numsSize, target, i + 1, sum - nums[i], total, memo);
279
280 memo[i][index] = add + sub;
281
282 return memo[i][index];
283}
284
285int findTargetSumWays(int* nums, int numsSize, int target) {
286 int total = 0;
287
288 for (int i = 0; i < numsSize; i++) {
289 total += nums[i];
290 }
291
292 if (target > total || target < -total) {
293 return 0;
294 }
295
296 int cols = 2 * total + 1;
297
298 int** memo = malloc(numsSize * sizeof(int*));
299
300 for (int i = 0; i < numsSize; i++) {
301 memo[i] = malloc(cols * sizeof(int));
302
303 for (int j = 0; j < cols; j++) {
304 memo[i][j] = -1;
305 }
306 }
307
308 int ans = dfs(nums, numsSize, target, 0, 0, total, memo);
309
310 for (int i = 0; i < numsSize; i++) {
311 free(memo[i]);
312 }
313
314 free(memo);
315
316 return ans;
317}
318
319目标和易错点
320易错点 1:方案数问题用加法
321
322目标和问:
323
324有多少种方案
325
326所以转移是:
327
328add + sub
329
330或者:
331
332dp[j] += dp[j - x]
333
334不是 max。
335
336易错点 2:target 可能是负数
337
338如果用记忆化搜索,sum 可能为负数,要加偏移量。
339
340sum + total
341易错点 3:背包转换时要判断奇偶
342
343如果:
344
345target + total
346
347是奇数,那么:
348
349positive = (target + total) / 2
350
351不是整数,直接返回 0。
352
353易错点 4:背包容量是 positive或者定义的capacity
354
355不是 target。
356
357是:
358
359positive = (target + total) / 2
360
361494 目标和:
362题型:
363 计数 DP / 01 背包方案数
364
365递归定义:
366 dfs(i, sum)
367 表示处理到第 i 个数,当前和为 sum,最后凑成 target 的方案数。
368
369递归转移:
370 dfs(i, sum)
371 = dfs(i + 1, sum + nums[i])
372 + dfs(i + 1, sum - nums[i])
373
374背包转换:
375 positive - negative = target
376 positive + negative = total
377
378 positive = (target + total) / 2
379
380转换后:
381 从 nums 中选一些数,使和为 positive,求方案数。
382
383背包状态:
384 dp[j] 表示凑出 j 的方案数。
385
386转移:
387 dp[j] += dp[j - nums[i]]
388
389遍历:
390 j 倒序。
391
392目标和的本质是:每个数前面选 + 或 -,计数所有能到 target 的路径;也可以转成 01 背包的选数方案数。
393
394题目 问什么 决策 转移核心
395198 打家劫舍 最大收益 偷 / 不偷 max
396494 目标和 方案数量 加正号 / 加负号 +
494 目标和转 0-1 背包
- 前提条件
每个数有两种选择:
加正号:选到正号集合 P
加负号:选到负号集合 N
设:
所有数总和 = s
正号集合的和 = p
负号集合的和 = s - p
题目要求:正号和 - 负号和 = target
所以:
p - (s - p) = target
展开:
2p - s = target
所以:
p = (s + target) / 2
也就是转化成两个值不同情况的取舍问题
2. 什么情况下无解?
因为:
p = (s + target) / 2
所以必须满足:
s + target >= 0
并且:
(s + target) % 2 == 0
更完整地说:
如果 target > s 或 target < -s,也无解。
例如:
nums = [1,2,3]
s = 6
target = 10
最大也只能得到 6,不可能得到 10。
- 为什么变成 0-1 背包?
原题是:每个数前面选 + 或 -
等价于:从 nums 里选一些数放进正号集合 P
要求:
选出来的数的和 = p
其中:
p = (s + target) / 2
所以变成:从数组中选一些数,使它们的和等于 p,问有多少种选法。
这就是 0-1 背包的计数版本的转化理解。
- 和普通 0-1 背包的区别
普通 0-1 背包常见问法:容量不超过 capacity 时,最大价值是多少?
所以它是:最大值问题
转移用:max(...)
目标和这题问的是:
有多少种选法能刚好凑出 p?
所以它是:方案数问题
转移用:+
6. dp[j] 怎么理解?
定义:dp[j] 表示:从已经看过的数字里,凑出和 j 的方案数。
初始化:
dp[0] = 1;
意思是:什么都不选,凑出 0,有 1 种方案。
这点非常重要。
- 转移怎么理解?
对于当前数:
x = nums[i]
要凑出 j,有两类方案:
-
不选 x:
原来 dp[j] 里的方案还在。
-
选 x:
那么之前必须先凑出 j - x。
这些方案数量是 dp[j - x]。
所以dp[j] = dp[j] + dp[j - x];
也就是:
dp[j] += dp[j - x];
这就是“方案数相加”。
- 为什么要倒序?
因为每个元素只能用一次。
for (int j = capacity; j >= x; j--) {
dp[j] += dp[j - x];
}
倒序可以保证:
dp[j - x] 还是上一轮的结果,
不会让当前 nums[i] 被重复使用。
如果正序,就会变成完全背包,当前元素可能被用多次。
也就是说本题把正负号两个情况转换成了01背包,正号集合和 = p,负号集合和 = s-p
则:p-(s-p)=target
推出:
p=(s+target)/2
问题转化:
从nums中选一些数
使其和为capacity
capacity=(s+target)/2
这样之后Dp数组里面总是有两种情况选x和不选x,这样就不断往前回溯,倒序是为了保证每次更新
dp[0]是默认什么都不选的情况,也就是空集,这种情况是默认为1,后续是选进来的情况加入集合,capacity作为选出符合要求dp的情况,其实默认就是选出正号集合的和
9. C 语言作答
#include <stdlib.h>
int findTargetSumWays(int* nums, int numsSize, int target) {
int s = 0;
for (int i = 0; i < numsSize; i++) {
s += nums[i];
}
if (target > s || target < -s) {
return 0;
}
if ((s + target) % 2 != 0) {
return 0;
}
int capacity = (s + target) / 2;
int* dp = malloc((capacity + 1) * sizeof(int));
for (int j = 0; j <= capacity; j++) {
dp[j] = 0;
}
dp[0] = 1;
for (int i = 0; i < numsSize; i++) {
int x = nums[i];
for (int j = capacity; j >= x; j--) {
dp[j] += dp[j - x];
}
}
int ans = dp[capacity];
free(dp);
return ans;
}
二、拿py理解一下
-
先想递归
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
s = sum(nums) - abs(target)
if s < 0 or s % 2:
return 0
@cache # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
def dfs(i: int, c: int) -> int:
if i < 0:
return 1 if c == 0 else 0
if c < nums[i]:
return dfs(i - 1, c) # 只能不选
return dfs(i - 1, c) + dfs(i - 1, c - nums[i]) # 不选 + 选
m = s // 2 # 背包容量
return dfs(len(nums) - 1, m)
定义:dfs(i, sum)
表示:
处理到第 i 个数时,当前和是 sum,最后能凑成 target 的方案数。
对于 nums[i] 有两个选择。
选择一:加 +
dfs(i + 1, sum + nums[i])
选择二:加 -
dfs(i + 1, sum - nums[i])
所以:
dfs(i, sum)= dfs(i + 1, sum + nums[i])+ dfs(i + 1, sum - nums[i])
为什么是加?
因为题目问:
有多少种方案
所以左右两种选择的方案数要加起来。
- 记忆化搜索:处理负数 sum
问题是:sum 可能是负数,不能直接当数组下标。
所以要加一个偏移量。
设:total = nums 所有元素之和
那么 sum 的范围是:
[-total, total]
可以用:
sum + total
把负数下标平移成非负数。
例如:
sum = -3
total = 10
index = sum + total = 7
5. 记忆化搜索 C 代码
这里用二维数组:
memo[i][sum + total]
表示:
处理到 i,当前和为 sum 时的方案数。
#include <stdlib.h>
int dfs(int* nums, int numsSize, int target, int i, int sum,
int total, int** memo) {
if (i == numsSize) {
if (sum == target) {
return 1;
} else {
return 0;
}
}
int index = sum + total;
if (memo[i][index] != -1) {
return memo[i][index];
}
int add = dfs(nums, numsSize, target, i + 1, sum + nums[i], total, memo);
int sub = dfs(nums, numsSize, target, i + 1, sum - nums[i], total, memo);
memo[i][index] = add + sub;
return memo[i][index];
}
int findTargetSumWays(int* nums, int numsSize, int target) {
int total = 0;
for (int i = 0; i < numsSize; i++) {
total += nums[i];
}
if (target > total || target < -total) {
return 0;
}
int cols = 2 * total + 1;
int** memo = malloc(numsSize * sizeof(int*));
for (int i = 0; i < numsSize; i++) {
memo[i] = malloc(cols * sizeof(int));
for (int j = 0; j < cols; j++) {
memo[i][j] = -1;
}
}
int ans = dfs(nums, numsSize, target, 0, 0, total, memo);
for (int i = 0; i < numsSize; i++) {
free(memo[i]);
}
free(memo);
return ans;
}
目标和易错点
易错点 1:方案数问题用加法
目标和问:
有多少种方案
所以转移是:
add + sub
或者:
dp[j] += dp[j - x]
不是 max。
易错点 2:target 可能是负数
如果用记忆化搜索,sum 可能为负数,要加偏移量。
sum + total
易错点 3:背包转换时要判断奇偶
如果:
target + total
是奇数,那么:
positive = (target + total) / 2
不是整数,直接返回 0。
易错点 4:背包容量是 positive或者定义的capacity
不是 target。
是:
positive = (target + total) / 2
494 目标和:
题型:
计数 DP / 01 背包方案数
递归定义:
dfs(i, sum)
表示处理到第 i 个数,当前和为 sum,最后凑成 target 的方案数。
递归转移:
dfs(i, sum)
= dfs(i + 1, sum + nums[i])
+ dfs(i + 1, sum - nums[i])
背包转换:
positive - negative = target
positive + negative = total
positive = (target + total) / 2
转换后:
从 nums 中选一些数,使和为 positive,求方案数。
背包状态:
dp[j] 表示凑出 j 的方案数。
转移:
dp[j] += dp[j - nums[i]]
遍历:
j 倒序。
目标和的本质是:每个数前面选 + 或 -,计数所有能到 target 的路径;也可以转成 01 背包的选数方案数。
题目 问什么 决策 转移核心
198 打家劫舍 最大收益 偷 / 不偷 max
494 目标和 方案数量 加正号 / 加负号 +