我们来继续使用我们的框架。
当总金额数为0的时候,所需硬币数为0.
“原问题和子问题中会变化的变量”
根据题意,问题中的变量就是”最少硬币数“。
“导致状态变化的行为”
在这里就是”选择不同面额的硬币“
一般来说,都是题中问什么,dp函数设成什么。在这里定义dp函数为:
dp(n) : 凑成总金额 n 所需的硬币数。
接下来我们就可以写出状态转移方程,同时记得使用 [分解问题] 的思想。
dp(n) = dp(n - i) + 1 // 凑成金额n所需硬币数 =? 凑成金额(n-i)所需硬币数 + 选用的面值为i的硬币
在实际使用中,因为做选择时是从硬币集合中做选择,所以要将硬币集合也作为一个参数。?
class Solution {
public int coinChange(int[] coins, int amount) {
return dp(coins, amount);
}
//dp函数返回值的定义:凑出金额amount的硬币数
public int dp(int[] coins, int amount){
//base case
if(amount == 0){
return 0;
}
if(amount < 0){
return -1;
}
int res = Integer.MAX_VALUE;
//遍历选择列表做选择
for(int coin : coins){
int subProblem = dp(coins,amount - coin);
if(subProblem == -1){
continue;
}
//寻找最优解
res = Math.min(res,subProblem + 1);
}
return res == Integer.MAX_VALUE ? -1 : res;
}
}
class Solution {
int[] memo;
public int coinChange(int[] coins, int amount) {
memo = new int[amount + 1];
Arrays.fill(memo,-666);
return dp(coins,amount);
}
int dp(int[] coins, int amount){
/*使用备忘录数组后,base case 出现了新的条件*/
if (amount == 0) return 0;
if (amount < 0) return -1;
if (memo[amount] != -666){
return memo[amount];
}
/*接下来的判定照旧*/
int res = Integer.MAX_VALUE;
for(int coin : coins){
int subProblem = dp(coins, amount - coin);
if (subProblem == -1) continue;
res = Math.min(res,subProblem + 1);
}
/*将计算结果存入备忘录*/
memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res;
return memo[amount];
}
}
class Solution {
int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
// base case
dp[0] = 0;
// 外层 for 循环在遍历所有状态的所有取值
/*外层循环什么意思?是金额!
dp数组的下标是金额,dp数组的元素是硬币数*/
for (int i = 0; i < dp.length; i++) {
// 内层 for 循环在求所有选择的最小值
for (int coin : coins) {
// 子问题无解,跳过
if (i - coin < 0) {
continue;
}
dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
}
为啥 dp 数组中的值都初始化为 amount + 1 呢,因为凑成 amount 金额的硬币数最多只可能等于 amount(全用 1 元面值的硬币),所以初始化为 amount + 1 就相当于初始化为正无穷,便于后续取最小值。
为啥不直接初始化为 int 型的最大值 Integer.MAX_VALUE 呢? 因为后面有 dp[i - coin] + 1,这就会导致整型溢出。