322. 零钱兑换
讲解链接:代码随想录
和昨天做过的零钱对换不太一样 昨天的零钱兑换是完全背包里的球排列问题 这个是求在指定的背包容量内 求最小的组合数
动态规划五部曲
1 定义dp方程 我们假设用了dp[j]个硬币去凑j容量的背包 要求dp[j]最小
2 推导递推公式 首先最少用j-coins[i]个硬币来凑 dp [j - coins[i]] 容量的金额(背包)(不加上他本身的情况) 那我再加上一个coins[i] 就是当前dp[j] 所需要的最小硬币个数 dp[j] = dp[j - coins[i]] + 1;
3 如何初始化dp 没有amount就没有硬币j 所以dp[0] = 0 其余的 因为要 求得 达到 amount(j) 金额所需的最小硬币个数 dp[j] 题目要求 最小 一定要用Math.min 只能用MAX_VALUE定义其他元素
4 确定遍历顺序 这里先遍历物品再遍历背包(求组合)还是先遍历背包再遍历物品(求排列)都可以 因为我只需要求他的最小个数 不需要组合或者排列数
5 试试推导一下递推公式 amount = 5 arr = [1 , 2 , 5]
dp[0] = 0 dp[1] = 1 dp[2] = 1 dp[3] = 2 dp[4] = 2 dp[5] = 1 题目求dp[amount] min值 直接上代码
Java:
class Solution {
public int coinChange(int[] coins, int amount) {
int max = Integer.MAX_VALUE;
int dp[] = new int[amount];
//全部初始化为最大值
for(int i = 0; i < dp.length; i++){
dp[i] = max;
}
//当金额为0 个数也为0
dp[0] = 0;
for(int i = 0; i < coins.length; i++){
//先遍历物品 再遍历背包
for(int j = coins[i]; j <= amount; j++){
//只有dp[j - coins[i]不是初始最大值时 才有意义取该位置
if(dp[j - coins[i]] != max){
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
}
//凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],
//那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1
// 就是dp[j](考虑coins[i])
//例子 amount = 5 arr = [1,2,5];
//dp[0] = 0 dp[1] = 1 dp[2] = 2.....
return dp[amount] == max ? -1 : dp[amount];
}
}
说人话就是 求到达背包容量的最小负重数
279.完全平方数
题目链接:279. 完全平方数 - 力扣(LeetCode)
讲解链接:代码随想录
和上面的几乎一致
定义上 总和为j的平方数的最少个数是dp[j]
递推公式上 最少个数 为 得到当前不考虑其平方数本身的最小个数 + 考虑当前平方数的个数(就是1) dp[j] = dp[j - i * i] + 1(求其最小值 还是要除 dp[0]外其余都为MAX_VALUE)
dp[0]表示 和为0的完全平方数的最小数量,那么dp[0]一定是0。
有同学问题,那0 * 0 也算是一种啊,为啥dp[0] 就是 0呢?
看题目描述,找到若干个完全平方数(比如 1, 4, 9, 16, ...),题目描述中可没说要从0开始,dp[0]=0完全是为了递推公式。
非0下标的dp[j]应该是多少呢?
从递归公式dp[j] = min(dp[j - i * i] + 1, dp[j]);中可以看出每次dp[j]都要选最小的,所以非0下标的dp[j]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖。
Java代码:
class Solution {
public int numSquares(int n) {
int max = Integer.MAX_VALUE;
int[] dp = new int[n + 1];
//初始化
dp[0] = 0;
for(int i = 1; i < dp.length; i++){
dp[i] = max;
}
//先遍历物品 再遍历背包
for(int i = 1; i * i <= n; i++){
for(int j = i * i; j <= n; j++){
dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
}
}
return dp[n];
}
}
139.单词拆分
讲解链接:代码随想录
自己干想没感觉 问了ai
清楚多了
Java:
class Solution{
public boolean wordBreak(String s, List<String> wordDict){
HashSet<String> set = new HashSet<>(wordDict);
boolean[] valid = new boolean[s.length() + 1];
valid[0] = true;
for(int i = 1; i <= s.length(); i++){
for(int j = 0; j < i && !valid[i]; j++){
if(set.contains(s.substring(j, i)) && valid[j]){
valid[i] = true;
}
}
}
return valid[s.length()];
}
}
背包问题总结篇
背包问题是动态规划里的非常重要的一部分,所以我把背包问题单独总结一下,等动态规划专题更新完之后,我们还会在整体总结一波动态规划。
关于这几种常见的背包,其关系如下:
通过这个图,可以很清晰分清这几种常见背包之间的关系。
在讲解背包问题的时候,我们都是按照如下五部来逐步分析,相信大家也体会到,把这五部都搞透了,算是对动规来理解深入了。
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性,所以下面我从这两点来对背包问题做一做总结。
#背包递推公式
问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:
问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:
- 动态规划:494.目标和(opens new window)
- 动态规划:518. 零钱兑换 II(opens new window)
- 动态规划:377.组合总和Ⅳ(opens new window)
- 动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)
问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:
问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:
#遍历顺序
#01背包
在动态规划:关于01背包问题,你该了解这些! (opens new window)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
和动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!
#完全背包
说完01背包,再看看完全背包。
在动态规划:关于完全背包,你该了解这些! (opens new window)中,讲解了纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
相关题目如下:
- 求组合数:动态规划:518.零钱兑换II(opens new window)
- 求排列数:动态规划:377. 组合总和 Ⅳ (opens new window)、动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)
如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:
对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了。
#总结
这篇背包问题总结篇是对背包问题的高度概括,讲最关键的两部:递推公式和遍历顺序,结合力扣上的题目全都抽象出来了。
而且每一个点,我都给出了对应的力扣题目。
最后如果你想了解多重背包,可以看这篇动态规划:关于多重背包,你该了解这些! (opens new window),力扣上还没有多重背包的题目,也不是面试考察的重点。
如果把我本篇总结出来的内容都掌握的话,可以说对背包问题理解的就很深刻了,用来对付面试中的背包问题绰绰有余!
背包问题总结: