1完全背包:零钱兑换
2先来欣赏一下灵神的代码
3class Solution:
4 def coinChange(self, coins: List[int], amount: int) -> int:
5 @cache # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
6 def dfs(i: int, c: int) -> int:
7 if i < 0:
8 return 0 if c == 0 else inf
9 #相当于凑不出来就不选
10 if c < coins[i]: # 凑得剩余钱没有币值大只能不选
11 return dfs(i - 1, c)
12 # 不选 vs 继续选
13 return min(dfs(i - 1, c), dfs(i, c - coins[i]) + 1)
14
15 ans = dfs(len(coins) - 1, amount)
16 return ans if ans < inf else -1
17
18对应 LeetCode 322:零钱兑换。
19
20一、前提条件
21
22题目给:
23
24int* coins;
25int coinsSize;
26int amount;
27
28含义:
29
30coins[i] 表示第 i 种硬币面额
31amount 表示要凑出的总金额
32
33要求:用最少数量的硬币凑出 amount。
34
35注意:每种硬币可以使用无限次。(这就是完全背包。)
36
37二、核心概念
381. 0-1 背包
39每个物品只能选一次。
40
41比如 494 目标和转背包时:
42
43每个 nums[i] 只能选一次。
44
45所以是 0-1 背包。
46
472. 完全背包
48每个物品可以选无限次。
49
50零钱兑换中:
51
52硬币 1 可以用很多次
53硬币 2 可以用很多次
54硬币 5 可以用很多次
55
56所以是完全背包。
57
58三、先看看灵神的思路:先想递归
59
60不要一上来想 DP。
61先定义递归:
62
63dfs(i, c)
64
65表示:
66
67只考虑 coins[0...i] 这些硬币,
68凑出金额 c,
69最少需要多少个硬币。
70
71例如:
72
73coins = [1,2,5]
74amount = 11
75
76那么:
77
78dfs(2, 11)
79
80表示:
81
82只考虑硬币 1、2、5,
83凑出金额 11,
84最少需要多少个硬币。
85
86四、当前决策
87现在考虑第 i 种硬币:coins[i]
88
89有两个选择。
90选择一:不选当前硬币
91
92既然不选 coins[i],那就只能考虑前面的硬币:
93
94dfs(i - 1, c)
95意思是:
96
97不用 coins[i],
98只用 coins[0...i-1],
99凑出 c。
100选择二:选当前硬币
101
102如果选了一个 coins[i],金额减少:
103
104c - coins[i]
105
106硬币数量加 1。
107
108因为完全背包可以重复选当前硬币,所以还是停留在 i:
109
110dfs(i, c - coins[i]) + 1
111
112注意这里是:dfs(i, c - coins[i])
113
114不是:dfs(i - 1, c - coins[i])
115
116因为当前硬币还能继续用。
117
118五、递归方程
119
120所以:
121
122dfs(i, c)=min( dfs(i - 1, c), dfs(i, c - coins[i]) + 1)
123
124含义:不选当前硬币,用i-1个里面的硬币选,和选当前硬币每选一次,要凑的钱款-去币值,然后次数加一
125
126两种情况取最小硬币数。
127
128因为题目问:最少需要多少个硬币
129
130所以这里用:min(...)
131
132六、递归边界
1331. 金额刚好凑完
134if (c == 0) return 0;
135
136意思是:凑出 0 元,不需要硬币。
137
1382. 没有硬币可选,或者金额变负
139if (i < 0 || c < 0) return INF;
140
141意思是:凑不出来。
142
143这里不能返回 0。
144
145因为返回 0 会被误认为:
146
147用了 0 个硬币就凑出来了。
148
149所以用一个很大的数:
150
151#define INF 1000000000
152
153表示无解。
154
155七、记忆化搜索
156
157递归会重复计算,所以加:
158
159memo[i][c]
160
161含义:
162
163memo[i][c] = dfs(i, c)
164
165也就是:
166
167只考虑 coins[0...i],
168凑出金额 c,
169最少需要多少个硬币。
170
171八、从记忆化搜索翻译成 DP 数组
172
173dfs 返回什么,dp 就表示什么。
174
175递归里:dfs(i, c)
176
177表示:
178
179只考虑 coins[0...i],
180凑出金额 c,
181最少需要多少个硬币。
182
183所以 DP 定义:
184
185dp[i][c]
186
187表示:
188
189只考虑 coins[0...i],
190凑出金额 c,
191最少需要多少个硬币。
192
193递归方程:
194
195dfs(i, c)=min( dfs(i - 1, c), dfs(i, c - coins[i]) + 1)
196
197翻译成 DP:
198
199dp[i][c]=min( dp[i - 1][c], dp[i][c - coins[i]] + 1)
200九、二维 DP 代码
201
202
203#include <stdlib.h>
204
205#define INF 1000000000
206
207int min(int a, int b) {
208 return a < b ? a : b;
209}
210
211int coinChange(int* coins, int coinsSize, int amount) {
212 int** dp = malloc(coinsSize * sizeof(int*));
213
214 for (int i = 0; i < coinsSize; i++) {
215 dp[i] = malloc((amount + 1) * sizeof(int));
216 }
217//动态分配二维数组给dp
218 for (int i = 0; i < coinsSize; i++) {
219 for (int c = 0; c <= amount; c++) {
220 dp[i][c] = INF;
221 }
222 }
223//先全部初始化成极大值
224 for (int i = 0; i < coinsSize; i++) {
225 dp[i][0] = 0;
226 }
227//然后给0定义,默认凑0元需要用0个硬币,本题不是01背包,所以这里的情况是刚好去全凑完是0
228 for (int c = 1; c <= amount; c++) {
229 if (c % coins[0] == 0) {
230 dp[0][c] = c / coins[0];
231 }
232 }
233//第一行要初始化要不后面无法用前面的值
234 for (int i = 1; i < coinsSize; i++) {
235 int x = coins[i];
236
237 for (int c = 1; c <= amount; c++) {
238 int notChoose = dp[i - 1][c];
239
240 int choose = INF;
241 if (c >= x && dp[i][c - x] != INF) {
242 choose = dp[i][c - x] + 1;
243 }
244//这里就是写dp状态方程
245 dp[i][c] = min(notChoose, choose);
246 }
247 }
248
249 int ans = dp[coinsSize - 1][amount];
250
251 for (int i = 0; i < coinsSize; i++) {
252 free(dp[i]);
253 }
254
255 free(dp);
256
257 if (ans == INF) {
258 return -1;
259 }
260
261 return ans;
262}
263一维:
264int min(int a , int b ){
265 if (a<=b){
266 return a;
267 }else{return b;
268 }
269}
270int coinChange(int* coins,int coinsSize,int amount)
271{
272 int INF = amount + 1;
273
274 int* dp = malloc((amount+1)*sizeof(int));
275
276 for(int i=0;i<=amount;i++)
277 {
278 dp[i]=INF;
279 }
280 dp[0]=0;
281
282 for(int i=0;i<coinsSize;i++)
283 {
284 int x = coins[i];
285
286 for(int c=x;c<=amount;c++)
287 {
288 dp[c]=min(
289 dp[c],dp[c-x]+1
290 //这里是相当于i-1行被第i行覆盖所以就压缩了一维
291
292 //但是保留了c-x那一列
293 );
294 }
295 }
296
297 int ans = dp[amount];
298
299 free(dp);
300
301 return ans==INF ? -1 : ans;
302}
303
304十、代码怎么理解?
3051. 初始化
306dp[i][0] = 0;
307
308意思是:
309
310不管用前几种硬币,
311凑出 0 元都需要 0 个硬币。
3122. 第一行初始化
313if (c % coins[0] == 0) {
314 dp[0][c] = c / coins[0];
315}
316
317意思是:
318
319只用第 0 种硬币时,
320如果金额 c 能被 coins[0] 整除,
321就可以凑出来。
322
323例如:
324
325coins[0] = 2
326
327那么:
328
329金额 4 可以用两个 2 凑出
330金额 6 可以用三个 2 凑出
331金额 3 凑不出
3323. 状态转移
333int notChoose = dp[i - 1][c];
334
335表示:
336
337不选当前硬币 coins[i]。
338int choose = dp[i][c - x] + 1;
339
340表示:
341
342选一个当前硬币 coins[i]。
343
344为什么是 dp[i][c - x]?
345
346因为:完全背包当前硬币可以重复用。
347
348所以选了一个 x 后,还是可以继续用第 i 种硬币。
349
350十一、例题完整讲解
351
352用:
353
354coins = [1, 2, 5]
355amount = 5
356
357目标:
358
359凑出 5 元,最少需要几个硬币。
3601. DP 表含义
361dp[i][c]
362
363表示:
364
365只使用 coins[0...i],
366凑出金额 c,
367最少需要多少个硬币。
368
369列是金额:
370
371c = 0 1 2 3 4 5
372
373行是硬币种类:
374
375i = 0,用硬币 1
376i = 1,用硬币 1、2
377i = 2,用硬币 1、2、5
3782. 初始化第一行:只用硬币 1
379
380只用硬币 1:
381
382凑 0:0 个
383凑 1:1 个 1
384凑 2:2 个 1
385凑 3:3 个 1
386凑 4:4 个 1
387凑 5:5 个 1
388
389所以第一行:
390
391 0 1 2 3 4 5
392coin1 0 1 2 3 4 5
3933. 处理硬币 2
394
395现在可以用:
396
3971 和 2
398c = 1
399
400凑 1:
401
402不选 2:dp[0][1] = 1
403选 2:金额不够,不能选
404
405所以:
406
407dp[1][1] = 1
408c = 2
409
410凑 2:
411
412不选 2:dp[0][2] = 2
413选 2:dp[1][0] + 1 = 0 + 1 = 1
414
415取最小:
416
417dp[1][2] = 1
418
419对应:
420
4212
422c = 3
423
424凑 3:
425
426不选 2:dp[0][3] = 3
427选 2:dp[1][1] + 1 = 1 + 1 = 2
428
429取最小:
430
431dp[1][3] = 2
432
433对应:
434
4351 + 2
436c = 4
437
438凑 4:
439
440不选 2:dp[0][4] = 4
441选 2:dp[1][2] + 1 = 1 + 1 = 2
442
443取最小:
444
445dp[1][4] = 2
446
447对应:
448
4492 + 2
450
451注意这里用了:dp[1][2]
452
453也就是本行的状态。
454
455这说明:
456
457硬币 2 可以重复使用。
458c = 5
459
460凑 5:
461
462不选 2:dp[0][5] = 5
463选 2:dp[1][3] + 1 = 2 + 1 = 3
464
465取最小:
466
467dp[1][5] = 3
468
469对应:1 + 2 + 2
470
471第二行:
472
473 0 1 2 3 4 5
474coin1 0 1 2 3 4 5
475coin2 0 1 1 2 2 3
4764. 处理硬币 5
477
478现在可以用:
479
4801、2、5
481c = 1
482不选 5:dp[1][1] = 1
483选 5:金额不够
484
485所以:
486
487dp[2][1] = 1
488c = 2
489不选 5:dp[1][2] = 1
490选 5:金额不够
491
492所以:
493
494dp[2][2] = 1
495c = 3
496不选 5:dp[1][3] = 2
497选 5:金额不够
498
499所以:
500
501dp[2][3] = 2
502c = 4
503不选 5:dp[1][4] = 2
504选 5:金额不够
505
506所以:
507
508dp[2][4] = 2
509c = 5
510不选 5:dp[1][5] = 3
511选 5:dp[2][0] + 1 = 0 + 1 = 1
512
513取最小:
514
515dp[2][5] = 1
516
517对应:
518
5195
520
521最终表:
522
523 0 1 2 3 4 5
524coin1 0 1 2 3 4 5
525coin2 0 1 1 2 2 3
526coin5 0 1 1 2 2 1
527
528答案:
529
530dp[2][5] = 1
531
532所以返回:
533
5341
535十二、易错点
536易错点 1:完全背包选当前物品后,i 不变
537
538完全背包:
539
540选当前硬币:
541dp[i][c - coins[i]] + 1
542
5430-1 背包:
544
545选当前物品:
546dp[i - 1][c - weight[i]] + value[i]
547
548区别:
549
550完全背包可以重复选,所以还是 i。
5510-1 背包只能选一次,所以变成 i - 1。
552易错点 2:本题是最小值问题
553
554所以用:
555
556min(...)
557
558不是:
559
560max(...)
561
562也不是:
563
564+
565易错点 3:无解不能用 0 表示
566
567无解要用:
568
569INF
570
571因为:
572
5730 表示真的用了 0 个硬币。
574
575例如:
576
577凑出 0 元,需要 0 个硬币。
578易错点 4:第一行要处理好
579
580只用第一种硬币时:
581
582能整除就可以凑出。
583不能整除就是 INF。
完全背包:零钱兑换
先来欣赏一下灵神的代码
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
@cache # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
def dfs(i: int, c: int) -> int:
if i < 0:
return 0 if c == 0 else inf
#相当于凑不出来就不选
if c < coins[i]: # 凑得剩余钱没有币值大只能不选
return dfs(i - 1, c)
# 不选 vs 继续选
return min(dfs(i - 1, c), dfs(i, c - coins[i]) + 1)
ans = dfs(len(coins) - 1, amount)
return ans if ans < inf else -1
对应 LeetCode 322:零钱兑换。
一、前提条件
题目给:
int* coins;
int coinsSize;
int amount;
含义:
coins[i] 表示第 i 种硬币面额
amount 表示要凑出的总金额
要求:用最少数量的硬币凑出 amount。
注意:每种硬币可以使用无限次。(这就是完全背包。)
二、核心概念
- 0-1 背包
每个物品只能选一次。
比如 494 目标和转背包时:
每个 nums[i] 只能选一次。
所以是 0-1 背包。
- 完全背包
每个物品可以选无限次。
零钱兑换中:
硬币 1 可以用很多次
硬币 2 可以用很多次
硬币 5 可以用很多次
所以是完全背包。
三、先看看灵神的思路:先想递归
不要一上来想 DP。
先定义递归:
dfs(i, c)
表示:
只考虑 coins[0...i] 这些硬币,
凑出金额 c,
最少需要多少个硬币。
例如:
coins = [1,2,5]
amount = 11
那么:
dfs(2, 11)
表示:
只考虑硬币 1、2、5,
凑出金额 11,
最少需要多少个硬币。
四、当前决策
现在考虑第 i 种硬币:coins[i]
有两个选择。
选择一:不选当前硬币
既然不选 coins[i],那就只能考虑前面的硬币:
dfs(i - 1, c)
意思是:
不用 coins[i],
只用 coins[0...i-1],
凑出 c。
选择二:选当前硬币
如果选了一个 coins[i],金额减少:
c - coins[i]
硬币数量加 1。
因为完全背包可以重复选当前硬币,所以还是停留在 i:
dfs(i, c - coins[i]) + 1
注意这里是:dfs(i, c - coins[i])
不是:dfs(i - 1, c - coins[i])
因为当前硬币还能继续用。
五、递归方程
所以:
dfs(i, c)=min( dfs(i - 1, c), dfs(i, c - coins[i]) + 1)
含义:不选当前硬币,用i-1个里面的硬币选,和选当前硬币每选一次,要凑的钱款-去币值,然后次数加一
两种情况取最小硬币数。
因为题目问:最少需要多少个硬币
所以这里用:min(...)
六、递归边界
- 金额刚好凑完
if (c == 0) return 0;
意思是:凑出 0 元,不需要硬币。
- 没有硬币可选,或者金额变负
if (i < 0 || c < 0) return INF;
意思是:凑不出来。
这里不能返回 0。
因为返回 0 会被误认为:
用了 0 个硬币就凑出来了。
所以用一个很大的数:
#define INF 1000000000
表示无解。
七、记忆化搜索
递归会重复计算,所以加:
memo[i][c]
含义:
memo[i][c] = dfs(i, c)
也就是:
只考虑 coins[0...i],
凑出金额 c,
最少需要多少个硬币。
八、从记忆化搜索翻译成 DP 数组
dfs 返回什么,dp 就表示什么。
递归里:dfs(i, c)
表示:
只考虑 coins[0...i],
凑出金额 c,
最少需要多少个硬币。
所以 DP 定义:
dp[i][c]
表示:
只考虑 coins[0...i],
凑出金额 c,
最少需要多少个硬币。
递归方程:
dfs(i, c)=min( dfs(i - 1, c), dfs(i, c - coins[i]) + 1)
翻译成 DP:
dp[i][c]=min( dp[i - 1][c], dp[i][c - coins[i]] + 1)
九、二维 DP 代码
#include <stdlib.h>
#define INF 1000000000
int min(int a, int b) {
return a < b ? a : b;
}
int coinChange(int* coins, int coinsSize, int amount) {
int** dp = malloc(coinsSize * sizeof(int*));
for (int i = 0; i < coinsSize; i++) {
dp[i] = malloc((amount + 1) * sizeof(int));
}
//动态分配二维数组给dp
for (int i = 0; i < coinsSize; i++) {
for (int c = 0; c <= amount; c++) {
dp[i][c] = INF;
}
}
//先全部初始化成极大值
for (int i = 0; i < coinsSize; i++) {
dp[i][0] = 0;
}
//然后给0定义,默认凑0元需要用0个硬币,本题不是01背包,所以这里的情况是刚好去全凑完是0
for (int c = 1; c <= amount; c++) {
if (c % coins[0] == 0) {
dp[0][c] = c / coins[0];
}
}
//第一行要初始化要不后面无法用前面的值
for (int i = 1; i < coinsSize; i++) {
int x = coins[i];
for (int c = 1; c <= amount; c++) {
int notChoose = dp[i - 1][c];
int choose = INF;
if (c >= x && dp[i][c - x] != INF) {
choose = dp[i][c - x] + 1;
}
//这里就是写dp状态方程
dp[i][c] = min(notChoose, choose);
}
}
int ans = dp[coinsSize - 1][amount];
for (int i = 0; i < coinsSize; i++) {
free(dp[i]);
}
free(dp);
if (ans == INF) {
return -1;
}
return ans;
}
一维:
int min(int a , int b ){
if (a<=b){
return a;
}else{return b;
}
}
int coinChange(int* coins,int coinsSize,int amount)
{
int INF = amount + 1;
int* dp = malloc((amount+1)*sizeof(int));
for(int i=0;i<=amount;i++)
{
dp[i]=INF;
}
dp[0]=0;
for(int i=0;i<coinsSize;i++)
{
int x = coins[i];
for(int c=x;c<=amount;c++)
{
dp[c]=min(
dp[c],dp[c-x]+1
//这里是相当于i-1行被第i行覆盖所以就压缩了一维
//但是保留了c-x那一列
);
}
}
int ans = dp[amount];
free(dp);
return ans==INF ? -1 : ans;
}
十、代码怎么理解?
- 初始化
dp[i][0] = 0;
意思是:
不管用前几种硬币,
凑出 0 元都需要 0 个硬币。
2. 第一行初始化
if (c % coins[0] == 0) {
dp[0][c] = c / coins[0];
}
意思是:
只用第 0 种硬币时,
如果金额 c 能被 coins[0] 整除,
就可以凑出来。
例如:
coins[0] = 2
那么:
金额 4 可以用两个 2 凑出
金额 6 可以用三个 2 凑出
金额 3 凑不出
3. 状态转移
int notChoose = dp[i - 1][c];
表示:
不选当前硬币 coins[i]。
int choose = dp[i][c - x] + 1;
表示:
选一个当前硬币 coins[i]。
为什么是 dp[i][c - x]?
因为:完全背包当前硬币可以重复用。
所以选了一个 x 后,还是可以继续用第 i 种硬币。
十一、例题完整讲解
用:
coins = [1, 2, 5]
amount = 5
目标:
凑出 5 元,最少需要几个硬币。
- DP 表含义
dp[i][c]
表示:
只使用 coins[0...i],
凑出金额 c,
最少需要多少个硬币。
列是金额:
c = 0 1 2 3 4 5
行是硬币种类:
i = 0,用硬币 1
i = 1,用硬币 1、2
i = 2,用硬币 1、2、5
2. 初始化第一行:只用硬币 1
只用硬币 1:
凑 0:0 个
凑 1:1 个 1
凑 2:2 个 1
凑 3:3 个 1
凑 4:4 个 1
凑 5:5 个 1
所以第一行:
0 1 2 3 4 5
coin1 0 1 2 3 4 5
3. 处理硬币 2
现在可以用:
1 和 2
c = 1
凑 1:
不选 2:dp[0][1] = 1
选 2:金额不够,不能选
所以:
dp[1][1] = 1
c = 2
凑 2:
不选 2:dp[0][2] = 2
选 2:dp[1][0] + 1 = 0 + 1 = 1
取最小:
dp[1][2] = 1
对应:
2
c = 3
凑 3:
不选 2:dp[0][3] = 3
选 2:dp[1][1] + 1 = 1 + 1 = 2
取最小:
dp[1][3] = 2
对应:
1 + 2
c = 4
凑 4:
不选 2:dp[0][4] = 4
选 2:dp[1][2] + 1 = 1 + 1 = 2
取最小:
dp[1][4] = 2
对应:
2 + 2
注意这里用了:dp[1][2]
也就是本行的状态。
这说明:
硬币 2 可以重复使用。
c = 5
凑 5:
不选 2:dp[0][5] = 5
选 2:dp[1][3] + 1 = 2 + 1 = 3
取最小:
dp[1][5] = 3
对应:1 + 2 + 2
第二行:
0 1 2 3 4 5
coin1 0 1 2 3 4 5
coin2 0 1 1 2 2 3
4. 处理硬币 5
现在可以用:
1、2、5
c = 1
不选 5:dp[1][1] = 1
选 5:金额不够
所以:
dp[2][1] = 1
c = 2
不选 5:dp[1][2] = 1
选 5:金额不够
所以:
dp[2][2] = 1
c = 3
不选 5:dp[1][3] = 2
选 5:金额不够
所以:
dp[2][3] = 2
c = 4
不选 5:dp[1][4] = 2
选 5:金额不够
所以:
dp[2][4] = 2
c = 5
不选 5:dp[1][5] = 3
选 5:dp[2][0] + 1 = 0 + 1 = 1
取最小:
dp[2][5] = 1
对应:
5
最终表:
0 1 2 3 4 5
coin1 0 1 2 3 4 5
coin2 0 1 1 2 2 3
coin5 0 1 1 2 2 1
答案:
dp[2][5] = 1
所以返回:
1
十二、易错点
易错点 1:完全背包选当前物品后,i 不变
完全背包:
选当前硬币:
dp[i][c - coins[i]] + 1
0-1 背包:
选当前物品:
dp[i - 1][c - weight[i]] + value[i]
区别:
完全背包可以重复选,所以还是 i。
0-1 背包只能选一次,所以变成 i - 1。
易错点 2:本题是最小值问题
所以用:
min(...)
不是:
max(...)
也不是:
易错点 3:无解不能用 0 表示
无解要用:
INF
因为:
0 表示真的用了 0 个硬币。
例如:
凑出 0 元,需要 0 个硬币。
易错点 4:第一行要处理好
只用第一种硬币时:
能整除就可以凑出。
不能整除就是 INF。