返回 LeetCode 刷题

Markdown File

完全背包解决兑换零钱问题

完全背包解决兑换零钱问题.md

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。
Rendered Preview

完全背包:零钱兑换
先来欣赏一下灵神的代码
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。

注意:每种硬币可以使用无限次。(这就是完全背包。)

二、核心概念

  1. 0-1 背包
    每个物品只能选一次。

比如 494 目标和转背包时:

每个 nums[i] 只能选一次。

所以是 0-1 背包。

  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(...)

六、递归边界

  1. 金额刚好凑完
    if (c == 0) return 0;

意思是:凑出 0 元,不需要硬币。

  1. 没有硬币可选,或者金额变负
    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;

}

十、代码怎么理解?

  1. 初始化
    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 元,最少需要几个硬币。

  1. 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。