问题描述:
先给你k种面值的硬币,面值分别为c1, c2, ..., ck,每种硬币的数量无限,再给一个总金额amount,问你最少需要几枚硬币凑出这个金额,如果不能凑出,算法返回-1.
1.暴力解法
问题分析:我们相求amount = 11时的最少硬币数,如果我知道amount=10(子问题)的最少硬币数,只需要把子问题的答案加1(在选择一枚面值为1的硬币)就是原问题的答案。
如何确定正确的状态转移方程:
(1)确定base case: 目标金额amount = 0的时候,返回0.
(2)确定“状态”,也就是原问题和子问题的变量:本题种硬币数量无限,金额给定,只有目标金额amount的状态会不断的向base case靠近,所以唯一的变量就是amount.
(3) 确定“选择”,也就是导致“状态”产生变化的行为:目标金额amount怎样发生变化,是因为你选择硬币,每选择一枚硬币,目标金额就会减少硬币对应的面额。所以说所有硬币的面值就是你的选择。
(4)明确dp函数/数组的定义:一般来说函数的参数就是状态转移的变量;函数的返回值就是题目中要求我们计算的量。本题中的状态只有一个,即“目标金额”,题目要求凑出目标金额所用的最少硬币数量就是我们计算的结果。
所以dp(n) 定义:输入一个目标金额n,返回凑出目标金额n的最少硬币数量。
public class ChooseCoin{
private int amount;
private List<Integer> coins;
int dp(int n) {
if (n == 0) {
return 0;
}
if (n < 0) {
return -1;
}
//正无穷
int res = Integer.MAX_VALUE;
for (Integer coin: coins) {
int subProblem = dp(n-coin);
if (subProblem == -1) {
continue;
}
res = Math.min(res, subProblem+1);
}
return res != Integer.MAX_VALUE ? res : -1;
}
}
2.带备忘录的递归解法
自顶向下
static int dp(Integer n, List<Integer> coins, Map<Integer, Integer> memo) {
//提前返回,减少重复计算
if (MapUtils.isNotEmpty(memo) && Objects.nonNull(memo.get(n))) {
return memo.get(n);
}
if (n == 0) {
return 0;
}
if (n < 0) {
return -1;
}
int res = Integer.MAX_VALUE;
for (Integer coin : coins) {
int temp = dp(n-coin, coins, memo);
if (temp == -1) {
continue;
}
res = Math.min(res, 1 + temp);
memo.put(n, res != Integer.MAX_VALUE ? res : -1);
}
return memo.get(n);
}
3. dp数组的迭代解法
dp数组的定义和前面的dp函数的定义类似,也是把“状态”,也就是目标金额作为变量。不过dp函数的“状态”体现在参数,而dp数组的“状态”体现在数组的索引。
dp数组定义:当目标金额为i时,至少需要dp[i]的硬币凑出。
static int coinChange(List<Integer> coins, int amount) {
if (amount < 0) {
return -1;
}
if (amount == 0) {
return 0;
}
//填充数组每个值为amount+1,代替无穷大
int[] dp = new int[amount +1];
Arrays.fill(dp, amount+1);
//base case
dp[0] = 0;
//外层for循环,遍历所有状态的所有取值
for (int i = 0; i< dp.length; i++ ) {
//内层for循环求所有选择的最小值
for (Integer coin : coins) {
//子问题无解,跳过
if (i - coin < 0) {
continue;
}
dp[i] = Math.min(dp[i], dp[i-coin] +1);
}
}
return dp[amount] == amount+1 ? -1 : dp[amount];
}