【LeetCode动态规划#09】完全背包问题实战,其二(零钱兑换和完全平方数--求物品放入个数)
零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
- 输入:coins = [1, 2, 5], amount = 11
- 输出:3
- 解释:11 = 5 + 5 + 1
示例 2:
- 输入:coins = [2], amount = 3
- 输出:-1
示例 3:
- 输入:coins = [1], amount = 0
- 输出:0
示例 4:
- 输入:coins = [1], amount = 1
- 输出:1
示例 5:
- 输入:coins = [1], amount = 2
- 输出:2
提示:
- 1 <= coins.length <= 12
- 1 <= coins[i] <= 2^31 - 1
- 0 <= amount <= 10^4
思路
硬币可以无限次使用,典型的完全背包问题(正序遍历)
注意这次是要求放入硬币的个数,最少个数
不多说直接看
五步走
1、确定dp数组的含义
dp[j]:能够凑出总金额j的最少硬币数
2、确定递推公式
如何推导出dp[j]? 这里还是可以分为放入硬币和不放入硬币两种情况(详见)
(1)不放入
如果不放入硬币,那么就还是取上一层遍历递推的结果,即dp[j] (体现用一维数组解决背包问题时,数组的“滚动”,可以联系 零钱兑换II 的解释)
即dp[j] = dp[j];
(2)放入硬币
假设我们现在遍历到j - coins[i]的容量,此时能凑出总金额j - coins[i]的最少硬币数是dp[j - coins[i]]
我们要求的是总金额为j(即容量为j)的最少硬币数,显然只需j - coins[i] + coins[i]即可,也就是再放一个硬币进背包。
那么相当于此时遍历到了背包容量j,又因为硬币放入背包时要占空间的,所以背包容量还是可以表示为j - coins[i]
此时,能凑出总金额j的最少硬币数是dp[j - coins[i]] + 1,加的1是指放入的这个硬币
即dp[j] = dp[j - coins[i]] + 1;
由于我们求的是最少硬币数,因此需要取这两者中更小的那个,那么递推公式就是
dp[j] = min(dp[j], dp[j - coins[i]] + 1);
3、初始化dp数组
这里不要惯性思维直接把dp[0]初始化为1,先看看题目
题目给的示例中明确了,dp[0] = 0
那其他位置呢?因为我们要找的的是最少硬币数,也就是递推过程中的最小值
为了不影响最小值的更新,应该将其余部分的值初始化为最大整数INT_MAX
4、确定遍历顺序
本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。
所以本题并不强调集合是组合还是排列。
代码
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//定义dp数组并初始化
//因为要找的是更小的递推值,所以初始值应该设为最大值
vector<int> dp(amount + 1, INT_MAX);
//由题目给的示例可知,dp[0]时应该是0
dp[0] = 0;
//遍历dp数组,顺序无所谓,因为要找的是使用硬币的最少个数
for(int i = 0; i < coins.size(); ++i){
for(int j = coins[i]; j <= amount; ++j){
if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过
dp[j] = min(dp[j], dp[j - coins[i]] + 1);//注意这里找的是硬币个数
}
}
}
//如果递推公式推导最后发现最后的值还是初始值,说明没有找到组合,返回-1
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
- 输入:n = 12
- 输出:3
- 解释:12 = 4 + 4 + 4
示例 2:
- 输入:n = 13
- 输出:2
- 解释:13 = 4 + 9
提示:
- 1 <= n <= 10^4
思路
"若干个完全平方数"---物品
"给定正整数 n"---背包容量
又由示例1可知,物品可以重复使用,因此这是一个完全背包
五步走
1、确定dp数组含义
dp[j]:给定整数j时,能放入使和为j的完全平方数的最少数量是dp[j]
2、确定递推公式
如何推导出dp[j]?
当然还是从两个方向:加上当前的完全平方数或者不加
所以dp[j] = min(dp[j], dp[j - i * i] + 1);这里求的是数量,所以仍然是加1
注意,所谓的"完全平方数"就是整数的平方,每个数的平方都是"完全平方数"
本题是要用"完全平方数"来装背包,所以好获取"完全平方数",只需在背包容量上限内不断循环一个自增的值,然后取平方即可(i*i)
3、初始化dp数组
联系dp定义,dp[0]:给定整数0时,能放入使和为j的完全平方数的最少数量是dp[0]
感觉dp[0]时应该是没有能放进去的(这里理由还没想好,以后可能有补充),并且题目是从1开始给的整数,就没有0这种情况
那么暂定dp[0] = 0(不行再改1),然后其余部分分的初始化还是INT_MAX(原因见上一题)
4、确定遍历顺序
完全背包,正序遍历
然后因为不是求排列或组合,遍历物品和容量时的顺序可以调换
代码
class Solution {
public:
int numSquares(int n) {
//定义dp数组
vector<int> dp(n + 1, INT_MAX);
//初始化
dp[0] = 0;
//遍历dp数组
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];
}
};
在代码实现中要注意两层for循环的处理细节
这里我们用来放入的“物品”是"完全平方数",也就是i*i
不像之前的题,我们从一个数组之类的地方把物品取出来放入就行
这里的物品需要使用循环值来“现用现算”
对于i*i <= n,其实就相当于之前的j = coins[i]; j <= amount
第二层for循环也是同理,将i*i这个整体看成物品即可