322.零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

1
2
3
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

示例 2:

1
2
输入:coins = [2], amount = 3
输出:-1

示例 3:

1
2
输入:coins = [1], amount = 0
输出:0

完全背包

在01背包中,物品只能被选择1次,而在完全背包中,物品能被选无数次

很自然的写出回溯表达式:

回溯代码:

这里的if (i < 0) return c == 0 ? 0 : Integer.MAX_VALUE / 2;这个表达式很关键,表明了如果无法找零,那么直接标记缓存数组为最大值,从而在接下来的最小值比较中总是不被选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.Arrays;

class Solution {
private int[] coins; // 存储硬币面值的数组
private int[][] memo; // 记忆化搜索,用于存储中间结果的缓存数组

// 主方法,找到组成金额的最少硬币数量
public int coinChange(int[] coins, int amount) {
this.coins = coins;
int n = coins.length;
// 初始化记忆化数组,大小为 [n][amount + 1]
memo = new int[n][amount + 1];
for (int[] row : memo)
Arrays.fill(row, -1); // 将所有元素初始化为 -1,表示未访问过
int ans = dfs(n - 1, amount); // 调用深度优先搜索方法
return ans < Integer.MAX_VALUE / 2 ? ans : -1; // 如果结果小于无穷大,返回结果,否则返回 -1 表示无法找零
}

// 深度优先搜索方法
private int dfs(int i, int c) {
// 基本条件:如果索引 i 小于 0,检查是否金额 c 为 0,如果是返回 0,否则返回一个极大值
if (i < 0) return c == 0 ? 0 : Integer.MAX_VALUE / 2; // 除 2 是防止 + 1 溢出
// 如果已经计算过这个状态,直接返回缓存中的结果
if (memo[i][c] != -1) return memo[i][c];
// 如果当前金额 c 小于当前硬币面值 coins[i],跳过当前硬币
if (c < coins[i]) return memo[i][c] = dfs(i - 1, c);
// 否则,选择最优解,比较不使用当前硬币和使用当前硬币的结果
return memo[i][c] = Math.min(dfs(i - 1, c), dfs(i, c - coins[i]) + 1);
}
}


  • 递推
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {

// 主方法,用于计算组成指定金额的最少硬币数量
public int coinChange(int[] coins, int amount) {
int n = coins.length; // 获取硬币种类的数量
int[][] f = new int[n + 1][amount + 1]; // 初始化二维数组 f,大小为 (n+1) x (amount+1)

// 初始化动态规划数组,设置初始值为 Integer.MAX_VALUE / 2
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= amount; j++) {
f[i][j] = Integer.MAX_VALUE / 2;
}
}
f[0][0] = 0; // 设置初始状态,不选择任何硬币时,金额为 0 的最小硬币数为 0

// 动态规划填充数组
for (int i = 0; i < n; i++) { // 遍历每个硬币
for (int c = 0; c <= amount; c++) { // 遍历每个金额
if (c < coins[i]) {
// 如果当前金额小于硬币面值,不能使用当前硬币
f[i + 1][c] = f[i][c];
} else {
// 否则,选择不使用当前硬币或使用当前硬币中的最小值
f[i + 1][c] = Math.min(f[i][c], f[i + 1][c - coins[i]] + 1);
}
}
}

// 返回结果,如果结果等于初始值,表示无法找零,返回 -1;否则返回结果
return f[n][amount] == Integer.MAX_VALUE / 2 ? -1 : f[n][amount];
}
}


  • dp

这里不需要像01背包一样从后往前遍历,因为完全背包问题的状态转移是f(i+1)f(i+1)的,并不完全依赖于f(i)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.Arrays;

class Solution {
// 主方法,用于计算组成指定金额的最少硬币数量
public int coinChange(int[] coins, int amount) {
// 初始化一维数组 f,大小为 amount + 1
int[] f = new int[amount + 1];
// 将数组 f 的所有元素填充为 Integer.MAX_VALUE / 2,表示初始状态下的最大值
Arrays.fill(f, Integer.MAX_VALUE / 2); // 除以 2 是防止下面 + 1 溢出
f[0] = 0; // 设置初始状态,金额为 0 的最小硬币数为 0

// 遍历每个硬币面值
for (int x : coins) {
// 遍历每个金额,从硬币面值开始,直到目标金额
for (int c = x; c <= amount; ++c) {
// 更新 f[c],选择不使用当前硬币和使用当前硬币中的最小值
f[c] = Math.min(f[c], f[c - x] + 1);
}
}

// 获取结果,f[amount] 表示组成目标金额的最少硬币数量
int ans = f[amount];
// 如果结果小于 Integer.MAX_VALUE / 2,返回结果;否则返回 -1 表示无法找零
return ans < Integer.MAX_VALUE / 2 ? ans : -1;
}
}