一、问题描述
小C有多种不同面值的硬币,每种硬币的数量是无限的。他希望知道,如何使用最少数量的硬币,凑出给定的总金额N。小C对硬币的组合方式很感兴趣,但他更希望在满足总金额的同时,使用的硬币数量尽可能少。
例如:小C有三种硬币,面值分别为 1, 2, 5。他需要凑出总金额为 18。一种最优的方案是使用三个 5 面值的硬币,一个 2 面值的硬币和一个 1 面值的硬币,总共五个硬币。
二、整体功能概述
这段Java代码定义了一个名为 solution 的方法,它接受一个整数数组 array 和一个整数 total 作为参数。其目的是从给定的整数数组中选择一些数,使得这些数的和等于 total,并以列表的形式返回这些数(如果存在这样的组合),如果不存在则返回 null。在主函数 main 中,通过调用 solution 方法并使用预定义的测试用例来验证方法的正确性。
三、编写过程与思路分析
- 初始化与预处理
Arrays.sort(array);
int[] dp = new int[total + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
(1)排序数组:首先使用 Arrays.sort(array) 对输入的整数数组 array 进行排序。这一步骤是为了后续的动态规划算法做准备,排序后的数组可以方便地按照从小到大的顺序处理元素,有助于优化算法的时间复杂度。
(2)动态规划数组初始化:创建一个名为 dp 的整数数组,其长度为 total + 1。这个数组用于存储中间结果,其中 dp[i] 表示组成总和为 i 所需的最少元素个数。初始时,将 dp 数组的所有元素填充为 Integer.MAX_VALUE,表示尚未计算出组成相应总和的最少元素个数,然后将 dp[0] 设置为0,因为组成总和为0不需要任何元素。
- 动态规划计算最少元素个数
for (int num : array) {
for (int i = num; i <= total; i++) {
if (dp[i - num]!= Integer.MAX_VALUE) {
dp[i] = Math.min(dp[i], dp[i - num] + 1);
}
}
}
(1)这部分是动态规划算法的核心。外层循环遍历数组 array 中的每个元素 num。对于每个 num,内层循环从 num 开始到 total,这是因为组成总和小于 num 的情况已经在前面的循环中处理过了(由于数组已排序)。
(2)在每次内层循环中,检查 dp[i - num] 是否不等于 Integer.MAX_VALUE,这意味着是否已经计算出组成总和为 i - num 的最少元素个数。如果是,那么就可以尝试更新 dp[i],即比较当前的 dp[i] 和 dp[i - num] + 1(表示使用一个 num 元素来组成总和为 i),取较小值更新 dp[i]。通过这样的动态规划过程,逐步计算出组成从0到 total 每个总和所需的最少元素个数。
- 检查是否存在解
if (dp[total] == Integer.MAX_VALUE) {
return null;
}
在完成动态规划计算后,检查 dp[total] 的值。如果 dp[total] 仍然是 Integer.MAX_VALUE,这意味着无法使用数组 array 中的元素组成总和为 total 的情况,所以直接返回 null。
- 构建结果列表
List<Integer> result = new ArrayList<>();
int remainingTotal = total;
for (int i = array.length - 1; i >= 0; i--) {
while (remainingTotal >= array[i] && dp[remainingTotal] == dp[remainingTotal - array[i]] + 1) {
result.add(array[i]);
remainingTotal -= array[i];
}
}
(1)首先创建一个空的 ArrayList 作为结果列表 result,并设置一个变量 remainingTotal 初始化为 total,用于跟踪剩余还需要组成的总和。
(2)然后从数组 array 的末尾开始向前遍历(因为数组是排序后的,从大到小尝试选择元素更容易得到结果)。在每次循环中,只要 remainingTotal 大于等于当前元素 array[i],并且满足 dp[remainingTotal] == dp[remainingTotal - array[i]] + 1,这意味着选择当前元素 array[i] 是组成总和为 remainingTotal 的最优选择(根据动态规划的结果),就将 array[i] 添加到结果列表 result 中,并更新 remainingTotal 的值,减去已选择的元素值。通过这样的循环,逐步构建出组成总和为 total 的元素列表。
- 主函数中的测试逻辑
public static void main(String[] args) {
// Add your test cases here
System.out.println(solution(new int[]{1,2,3,10},21).equals(List.of(10,10,1)));
System.out.println(solution(new int[]{1, 2, 5}, 18).equals(List.of(5, 5, 5, 2, 1)));
}
在主函数中,调用 solution 方法并传入不同的参数进行测试。对于每个测试用例,将 solution 方法的返回值与预期的 List 进行比较(使用 equals 方法),并将比较结果输出到控制台。这有助于验证 solution 方法在不同输入情况下的正确性。
四、总结
这段代码通过动态规划算法先计算组成给定总和所需的最少元素个数,然后根据动态规划的结果构建出组成总和的元素列表,并且在主函数中提供了测试用例来验证代码的正确性。这种算法在处理组合优化问题(如背包问题的一种变体)时非常有效,可以在合理的时间复杂度内找到满足条件的解或者判断无解的情况。