零钱兑换 II
零钱兑换 II是一个经典的完全背包问题。那么什么是完全背包呢?完全背包的意思是物品是可以无限使用的,背包仍然还是只能装载这么多。背包问题和完全背包问题的区别就是物品可使用次数,背包问题是只能使用一次,完全背包是不限次数。所以它们是非常相像的问题,只是细节上有一些不同。
那么既然是一个完全背包问题,这个问题肯定就是一个动态规划问题。既然是动态规划肯定就需要涉及状态转移和选择问题。
在这之前首先我们就需要定义dp[i][j],它的意思其实就是在只能选前i个硬币的面值的情况下,背包装满 j 金额的不同装法一共有多少种。
既然定义好了dp数组,下一步就是需要明确选择问题。选择什么?选择的就是到底要不要使用第i个面值的金币。那么就出现了两种情况。
①不使用第i个面值的硬币,那么背包能装满j金额就是dp[i-1][j]种,i-1就是使用前i-1个硬币就可以了,不使用第i个
②使用第i个面值的硬币,那么背包能装满j金额的总组合数就是dp[i][j-coins[i]].
相信你们在这里都会疑惑为什么第二种需要减去一个coins[i],因为这里需要确定使用了coin[i]这种面值的硬币,这样就保证了这里一定会使用第i个硬币,而后面的拼凑第i个硬币是可以重复出现的所以dp[i][j-coins[i]]的i是不需要减1的,所以这里也是完全背包问题和背包问题不一样的地方。
更重要一点是状态转移还没有结束。因为这里只是分别计算了使用还是不使用第i个硬币的总组合数。那么dp[i][j]实际上就是等于这两种情况的和。为什么?因为你确定了其中一种情况一定有第i个硬币,另一种没有。那么其实就是两大类情况了,所以这两个情况加起来才是总的组合数。
这里可能你还想问dp[i-2][j]呢?这种情况是不是也需要加进来呢?答案是不需要的,原因很简单,因为dp[i-1][j]已经包含了这种情况。翻译一下意思,然后自己计算一下就知道了。最后需要处理一下细节的问题,比如基本事件dp[?][0]也就是背包不能装东西的时候,只需要不装硬币就好了,也就是1种情况。第二种就是dp[0][?],你没有硬币了怎么凑?所以是0种。还有一个地方就是第i个硬币的面值比背包大的情况,这种情况只能使用前i-1个硬币的最佳组合数
代码(二维数组):
public static int change(int amount,int[] coins){
int n=coins.length;
// +1是为了所有的基本事件能覆盖到
int[][] dp=new int[n+1][amount+1];
// 初始化所有的值basecase
for(int i=0;i<=n;i++){
dp[i][0]=1;
}
// 动态规划
for(int i=1;i<=n;i++){
for(int j=1;j<=amount;j++){
if(j-coins[i-1]>=0){
dp[i][j]=dp[i-1][j]+dp[i][j-coins[i-1]];
}else{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[n][amount];
}
最后这一道题还能再优化,我们发现每个状态都是与上一列的状态和当前行 前面的状态有关(二维数组)。所以我们可以把它变成一个一维数组。其实思路一模一样,因为dp[j]=dp[j]+dp[j-coins[i]]里面的dp[j]还是上一行的那个,所以这个状态转移是可行的。而dp[j-coins[i]]是当前行前面已经计算过的,所以也是可行的。
代码(一维数组)
class Solution {
public int change(int amount, int[] coins) {
int n=coins.length;
int[] dp=new int[amount+1];
dp[0]=1;
for(int i=0;i<n;i++){
for(int j=1;j<=amount;j++){
if(j-coins[i]>=0){
dp[j]=dp[j]+dp[j-coins[i]];
}
}
}
return dp[amount];
}
}