问题描述
小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]
思路分析
这是一个典型的完全背包问题,可以使用动态规划来解决这个问题:
- 初始化一个数组
dp
,长度为amount + 1
,每个位置初始化为amount + 1
(这个值是大于任何可能硬币数量的,作为初始状态),除了dp[0]
,它应该初始化为0,因为凑出金额0不需要任何硬币。 - 遍历每种硬币的面值,对于每种面值,再遍历从该硬币面值到
amount
的所有金额,更新dp
数组。 - 对于每个
dp[i]
,我们检查是否可以用当前面值的硬币来减少达到金额i
所需的硬币数量。如果可以,则更新dp[i]
为min(dp[i], dp[i - coin] + 1)
。 - 完成动态规划数组后,我们可以通过回溯来找出使用了哪些硬币。从
dp[amount]
开始,如果dp[i]
是由dp[i - coin] + 1
得到的,那么我们就使用了面值为coin
的硬币。
代码注释
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Collections;
public class Main {
public static List<Integer> solution(int[] coins, int amount) {
// dp数组,用于存储达到每个金额所需的最少硬币数量
int[] dp = new int[amount + 1];
// prev数组,用于记录达到每个金额时使用的最后一个硬币面值
int[] prev = new int[amount + 1];
// 初始化dp数组,所有值设置为amount + 1
Arrays.fill(dp, amount + 1);
// 设置dp[0]为0,因为凑出金额0不需要任何硬币
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
// 如果当前硬币面值小于等于当前金额
if (coins[j] <= i) {
// 如果使用当前硬币可以减少所需硬币数量
if (dp[i] > dp[i - coins[j]] + 1) {
dp[i] = dp[i - coins[j]] + 1;
prev[i] = coins[j];
}
}
}
}
// 如果dp[amount]没有更新,说明无法凑出该金额
if (dp[amount] == amount + 1) {
return new ArrayList<>();
}
// 根据prev数组回溯找出硬币组合
List<Integer> result = new ArrayList<>();
for (int i = amount; i > 0; i -= prev[i]) {
result.add(prev[i]);
}
// 将结果排序,确保面值大的硬币在前
Collections.sort(result, Collections.reverseOrder());
return result;
}
public static void main(String[] args) {
System.out.println(solution(new int[]{1, 2, 5}, 18).equals(List.of(5, 5, 5, 2, 1)));
}
}