两道题思路上有相似之处,都是求得最少的种类方法,也就是说在完全背包里给定容量时,用最少的物品去装满背包。它和用最多的方法去装满背包也有一些相似,也就是说两者实际上是互通的。
322. 零钱兑换 - 力扣(LeetCode)https://leetcode.cn/problems/coin-change/用最少的纸张数量来凑齐目标数target,在第一次做这道题时候,很多朋友都感觉应该是模拟钱相加得到target,然后用计数器计数什么时候达到最少的纸币,很自然的想到应该要给数组排序,大数额钱放在前面,然后再用次大数额,以此类推,至少我一开始是这样想的。这样或许也是可以做出来的,不过我们本期要讲的是用完全背包的思路解答,我们来看动规五部曲分析,来体会动态规划的神奇之处。
dp数组的含义:dp【j】,j容量下对应的最少硬币个数
递推公式:递推公式和求取填满背包最多物品个数有几个,差不太多。我们是求最少那就是
dp【j】=min(dp【j】,dp【j-coins【i】】+1)
这里为什么要+1?原因在于我们并非模拟硬币的价值相加等于target,dp数组的含义就是此背包容量下,能承载的最少硬币个数有多少,我们比较当前背包容量的已有值的硬币个数与该容量减当前要遍历的硬币价值(也就是腾出背包空间,这一点不懂翻看前面的文章)+1,看它们谁更小取谁。推到最后得到的dp【target】就是我们要的值,即当前容量下我们可以取的最小硬币个数。看到这里,可能会有一个疑问,如何判断出来我们这个目标值是否真的被这些硬币数量填满了!我们并不是真的模拟硬币相加等于target,那我们是怎么知道这些硬币肯定能装满背包的呢?答案就在递推公式里,我们求大容量背包时,是由之前的小容量背包填充,也就是说在遍历target这么大的背包之前,一定有一个最小的背包已经被装满,它们可以是dp【1】也可以是dp【2】这是由硬币数组来决定的。我们得到了这个之后,推较大背包容量时,min的第二个值,dp【j-coins【i】】可以帮助我们找到已经填满的背包,那我们如何确定这个较大背包被填满?你遍历到不同硬币它减出来的数值不同,所以这一点不用担心,你用到这枚硬币时,它向前找如果找到的那个背包是有一个更新过的值,那么说明那个背包一定是满的,这个背包在这个硬币的基础上,就转化为之前的小背包钱个数+1,得到该背包的答案,如何判断之前背包是否是更新过的值?这一点就要说到dp数组初始化了!
dp数组初始化:没错初始化在本题型中,也显得尤为重要,事实上在求装满背包最多或者最少的物品时候,都是很重要的。最多物品时,我们是将数组初始化为0,这一点不只是在递推公式向后推导时,为了避免起始数值影响结果,其实如果推导时候,某一个背包容量一直为0也说明,该容量无法被推出!!这种求最少的题,我们将其全部初始化为INT_MAX原因也是一样的,一个是不影响递推公式的推导,另一个是为了告诉推到的时候,如果用到之前的小背包时候,小背包如果需要硬币个数为INT_MAX那么说明,此时我们遍历的该硬币价值,无法填满我们的背包。这时候就该看一看其他硬币了。当然dp【0】不能被初始化为INT_MAX因为题里已经告诉我们,目标值为0的时候,需要硬币个数为0。这也是我们如何推导dp【1】以及后面的关键所在(dp【0】如果是INT_MAX的话,无法向后推,因为一开始dp【1】初始化就是最大数,它的前面的小背包dp【0】也是最大数)。
遍历顺序:先遍历背包还是物品都没有问题。因为是完全背包,这里看不懂的朋友去看之前一期有专门比较完全背包和01背包的专题,但是有同学就要问了:即使是完全背包由于遍历顺序不同,不是也会有组合和排列的不同吗?知道这一点的同学确实很聪明,但是这里我们是求背包容量下最少物品个数,所以无论是怎么推考不考虑答案的顺序,都不能影响我们最后的答案!唯一需要注意的只有正向遍历背包!
dp打印:还是一笔带过,就是为了提醒我们,如果出现一些奇怪的bug,感觉代码完全可以通过,但是就是答案不对,那就考虑打印dp数组,看看是从哪个容量出了问题,再做分析。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int>dp(amount+1,INT_MAX);
dp[0]=0;
for(int i=0;i<coins.size();i++){
for(int j=1;j<=amount;j++){
if(j>=coins[i]&&dp[j-coins[i]]!=INT_MAX)
dp[j]=min(dp[j],dp[j-coins[i]]+1);
}
}
if(dp[amount]==INT_MAX)return -1;
return dp[amount];
}
};
279. 完全平方数 - 力扣(LeetCode)https://leetcode.cn/problems/perfect-squares/这一道题的思路和上一道题的思路完全相同,相信大家如果能对我上一道题的细致讲解完全吃透的话,应该不算难题。同样也是求装满背包用的最少完全平方数的数量。
dp数组的含义,递推公式,dp数组初始化,完全一样,遍历顺序也是一样的,但是遍历的for循环需要注意一下,我们求得是完全平方数,第一层循环控制数一点点自增,而不是直接给它变到平方数,因为这样有利于代码书写,当我们写第二层循环时候,再调整数据的平方。还有就是递推公式的思路是不变的,但是dp【j-coins【i】】变成了dp【j-i*i】,也就是减去这个平方数来查前面的背包是否有正确值,具体代码如下。
class Solution {
public:
int numSquares(int n) {
vector<int>dp(n+1,INT_MAX);
dp[0]=0;
for(int i=1;i*i<=n;i++){
for(int j=i*i;j<=n;j++){
dp[j]=min(dp[j],dp[j-i*i]+1);
}
}
return dp[n];
}
};
看到这里了,点个关注呗!如果大家有其他的不懂的题,可以翻看我之前的文章,关于一些常见算法,回溯,贪心,动态规划,往期文章都有讲解。