这道题是一个经典的零钱兑换问题,通常使用动态规划(Dynamic Programming, DP)来解决。动态规划是一种将问题分解成更小的子问题,并逐步构建解决方案的方法。在零钱兑换问题中,我们需要计算能够凑出给定总金额所需的最少硬币数量。
题目描述
小C有多种不同面值的硬币,每种硬币的数量是无限的。他希望知道如何使用最少数量的硬币凑出给定的总金额N。小C对硬币的组合方式很感兴趣,但他更希望在满足总金额的同时,使用的硬币数量尽可能少。
示例
- 输入:coins = [1, 2, 5], amount = 18
- 输出:5(可以使用三个 5 面值的硬币,一个 2 面值的硬币和一个 1 面值的硬币)
解决方案
1. 定义状态
- 我们用
dp[i]表示凑成总金额i所需的最少硬币数量。
2. 状态转移方程
- 对于每个硬币面值
coin,如果i大于等于coin,则更新dp[i]为dp[i - coin] + 1,其中dp[i - coin]表示凑成金额i - coin所需的最少硬币数量加上一个coin硬币。
3. 初始化
- 将
dp[0]初始化为0,因为凑成金额0所需的硬币数量为0。 - 其他
dp[i]初始化为一个大值(比如amount + 1),表示初始状态下无法凑成这些金额。
4. 返回结果
- 最终返回
dp[amount],如果dp[amount]大于amount,则表示无法凑成该金额,返回 -1,否则返回dp[amount]。
代码实现
以下是完整的Java代码实现:
java
import java.util.Arrays;
public class CoinChange {
public static void main(String[] args) {
int[] coins = {1, 2, 5};
int amount = 18;
System.out.println(minCoins(coins, amount)); // 输出应该是5
}
public static int minCoins(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // 用一个比 `amount` 大的值来初始化
dp[0] = 0; // 凑出金额 0 需要的硬币数量为0
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i >= coin) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
代码解释
-
初始化:
- 创建一个长度为
amount + 1的数组dp,并将所有值初始化为一个大于amount的值(这里是amount + 1)。 - 设置
dp[0] = 0,表示凑出金额0所需的硬币数量为0。
- 创建一个长度为
-
状态转移:
- 使用双层循环,外层循环遍历从1到
amount的每一个金额,内层循环遍历每一个硬币面值coin。 - 对于每一个金额
i,如果i大于等于coin,则更新dp[i]为dp[i - coin] + 1和dp[i]中的最小值。
- 使用双层循环,外层循环遍历从1到
-
返回结果:
- 最终结果存储在
dp[amount]中,如果dp[amount]的值大于amount,则表示无法凑出该金额,返回 -1,否则返回dp[amount]。
- 最终结果存储在
复杂度分析
-
时间复杂度:
- 外层循环遍历从1到
amount,内层循环遍历每一个硬币面值,因此时间复杂度为O(amount * coins.length)。
- 外层循环遍历从1到
-
空间复杂度:
- 使用了一个长度为
amount + 1的数组dp,因此空间复杂度为O(amount)。
- 使用了一个长度为
总结
通过动态规划的方法,我们可以有效地解决零钱兑换问题。动态规划的核心思想是将问题分解为更小的子问题,并逐步构建解决方案。在这个问题中,通过定义状态和状态转移方程,我们能够计算出凑成给定总金额所需的最少硬币数量。
这个解决方案的时间复杂度为 O(amount * coins.length),空间复杂度为 O(amount),能够高效地解决大规模数据的处理。