问题描述
小C有多种不同面值的硬币,每种硬币的数量是无限的。他希望知道,如何使用最少数量的硬币,凑出给定的总金额N。小C对硬币的组合方式很感兴趣,但他更希望在满足总金额的同时,使用的硬币数量尽可能少。
例如:小C有三种硬币,面值分别为 1, 2, 5。他需要凑出总金额为 18。一种最优的方案是使用三个 5 面值的硬币,一个 2 面值的硬币和一个 1 面值的硬币,总共五个硬币。
测试样例
样例1:
输入:
coins = [1, 2, 5], amount = 18
输出:[5, 5, 5, 2, 1]
样例2:
输入:
coins = [1, 3, 4], amount = 6
输出:[3, 3]
样例3:
输入:
coins = [5], amount = 10
输出:[5, 5]
问题理解
我们需要找到一种方法,使用最少数量的硬币来凑出给定的总金额 N。硬币的面值和数量是无限的。目标是找到一种组合方式,使得使用的硬币数量最少。
数据结构选择
-
动态规划数组
dp:dp[i]表示凑出金额i所需的最少硬币数。- 初始化
dp[0] = 0,表示凑出金额0不需要任何硬币。 - 其他
dp[i]初始化为一个较大的值(如Integer.MAX_VALUE),表示初始状态下无法凑出该金额。
-
辅助数组
usedCoins:usedCoins[i]记录凑出金额i时使用的最后一个硬币的面值。- 用于在找到最优解后回溯路径,记录使用的硬币组合。
算法步骤
-
初始化:
- 初始化
dp数组,dp[0] = 0,其他dp[i] = Integer.MAX_VALUE。 - 初始化
usedCoins数组,用于记录使用的硬币。
- 初始化
-
动态规划求解:
- 对于每个金额
i,遍历所有硬币面值coin。 - 如果
i >= coin且dp[i - coin]不是Integer.MAX_VALUE,则更新dp[i]和usedCoins[i]。
- 对于每个金额
-
回溯路径:
- 如果
dp[amount]仍然是Integer.MAX_VALUE,说明无法凑出该金额,返回空列表。 - 否则,从
amount开始回溯,记录使用的硬币组合。
- 如果
-
结果处理:
- 将回溯得到的硬币组合翻转,以正确的顺序返回。
代码实现
import java.util.ArrayList;
import java.util.List;
public class Main {
public static List<Integer> solution(int[] coins, int amount) {
// 初始化 dp 数组,dp[i] 表示凑出金额 i 所需的最少硬币数
int[] dp = new int[amount + 1];
// 初始化 usedCoins 数组,usedCoins[i] 表示凑出金额 i 时使用的最后一个硬币的面值
int[] usedCoins = new int[amount + 1];
// 将 dp 数组初始化为一个较大的值,表示初始状态下无法凑出该金额
for (int i = 1; i <= amount; i++) {
dp[i] = Integer.MAX_VALUE;
}
// 动态规划求解
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i >= coin && dp[i - coin] != Integer.MAX_VALUE) {
if (dp[i - coin] + 1 < dp[i]) {
dp[i] = dp[i - coin] + 1;
usedCoins[i] = coin; // 记录使用的硬币
}
}
}
}
// 如果 dp[amount] 仍然是 Integer.MAX_VALUE,说明无法凑出该金额
if (dp[amount] == Integer.MAX_VALUE) {
return new ArrayList<>();
}
// 回溯路径,记录使用的硬币组合
List<Integer> result = new ArrayList<>();
int currentAmount = amount;
while (currentAmount > 0) {
int coin = usedCoins[currentAmount];
result.add(coin);
currentAmount -= coin;
}
System.out.println(result);
//翻转数组,返回结果
for (int i = 0; i < result.size() / 2; i++) {
Integer temp = result.get(i);
result.set(i, result.get(result.size() - 1 - i));
result.set(result.size() - 1 - i, temp);
}
System.out.println(result);
return result;
}
public static void main(String[] args) {
// Add your test cases here
System.out.println(solution(new int[]{1, 2, 5}, 18).equals(List.of(5, 5, 5, 2, 1)));
}
}
复杂度分析
- 时间复杂度:
O(N * M),其中N是总金额,M是硬币面值的数量。 - 空间复杂度:
O(N),用于存储dp数组和usedCoins数组。
做题心得
- 这道题和力扣上322. 零钱兑换相似,力扣上的题是求硬币个数,这道题是求使用的硬币,因此需要在动态规划基础上用一个数组来记录使用到的硬币。