给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
解: 由于所需硬币数是不确定的,而且组合数也不定,所以需要穷举所有的组合。在这题中,硬币数是不定的,可以作为自变量n,硬币数n和总金额amount的对应关系为:
f(n) = amount
f(n)的最优解使用动态规划的最优子结构可以构造为f(n) = f(n-1) + coins[i],即找出n-1个硬币组合的最小值,也就是剩余金额的最小硬币数组合。使用自底向上迭代法重构f(n):
将金额数从0到amount作为自变量,求解当前金额下的最小硬币组合数的数学公式为:
f(i) = min{f(i), f(i-coin) + 1}, 其中:0 ≤ i ≤ amount+1,coin∈coins
根据上面的公式构建数组dp[amount+1],初始全置最大值amount+1是因为当dp[i] = amount+1时,最小硬币数组合查过了面额值amount,必然是没有满足amount的组合数,因此算法返回-1。
dp[i] = min(dp[i], dp[i - coin] + 1)
关于上面公式的解释:dp[i]表示当金额为i时的最小硬币数。dp[i - coin] + 1表示金额为 i 时,选择coin作为最小硬币数组合中的一个,所以金额 i 选定了一个面值为 coin 的硬币,数量确定了一个,需要 +1;而dp[i - coin] 为选定 coin 后剩余面额(i-coin)时的最小硬币数。
关于min(dp[i], dp[i - coin] + 1),为什么选择dp[i] 和 (dp[i - coin] + 1)中最小的一个呢?这是因为我们在初始时将整个dp数组除0位置外都赋值为amount+1,也就是说初始的时候dp[i] 和 dp[i - coin] 的值是相同的。如果给定硬币面额组合中没有满足,或刚好需要amount+1个硬币才满足金额为(i-coin)的时候,这个时候dp[i-coin]的是大于或等于dp[i],再加1就会大于dp[i],而我们需要的是最小组合数,此时需要选择dp[i]。
完整Java代码如下:
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
Arrays.fill(dp, amount+1); //数组所有元素置最大值
dp[0] = 0;
for(int i = 0; i < dp.length; i++){
for(int coin : coins){
if(i < coin) continue;
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
return (dp[amount] == (amount+1)) ? -1 : dp[amount];
}
}