目录
思路同零钱兑换+物品遍历for(int i = 0;i*i <= n;i++)
前沿:撰写博客的目的是为了再刷时回顾和进一步完善,其次才是以教为学,所以如果有些博客写的较简陋,是为了保持进度不得已而为之,还请大家多多见谅。
预:看到题目后的思路和实现的代码。
见:参考答案展示。
感思:对比答案后的思考,与之前做过的题目是否有关联。
行:
(1)对于没做出来的题目,阅读答案后重新做一遍;
(2)下次做题可以尝试改善的方向;
(3)有助于理解的相关的题目
优先级:做题进度>学习&总结>默写回顾>做题数量
题目回顾
动态规划相关章节
代码随想录刷LeetCode | day39 动态规划刷题回顾
动态规划五步法:
- 确定dp数组以及下标的含义
确定递推公式
dp数组如何初始化
确定遍历顺序
举例推导dp数组
1.完全背包理论
理论链接:完全背包理论
0-1背包指的是每个物品只能选择一次,求背包在容量j时的最大价值。
完全背包则是指每个物品能选择多次(相对于有放回),求背包在容量j的最大价值。
完全背包的重点在于遍历顺序,虽然对于纯完全背包问题,物品和容量谁在for循环里都行,与0-1背包逆序遍历不同,完全背包问题要正序遍历。
- 纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系。
- 改变循环内外顺序都不会影响装满背包的最大价值,只是影响覆盖计算的值次数不同罢了。
但实际出的算法中,完全背包的遍历顺序要根据题目来变化。
- 排序问题(组合总和 Ⅳ)时,不同顺序的组合都作为一种新的方案,此时容量要放在外循环,内循环则是物品,这样才能每次遍历时容量都能从全部物品中重新选择。
- 组合问题求次数→特有递推公式dp[j] += dp[j-weight[i]],则与排序问题相反,要保证每个组合只出现一次,则需要物品在外循环,内循环为容量,这样都只能遍历一次。
2.组合总和 Ⅳ
题目链接:377. 组合总和 Ⅳ
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target+1];
int len = nums.length;
dp[0] = 1;
for(int j = 1;j <= target;j++){
for(int i = 0;i < len;i++){
if(j >= nums[i]){
// System.out.println(dp[j]);
dp[j] += dp[j-nums[i]];
}
}
}
return dp[target];
}
}
3.零钱兑换 II
题目链接:518. 零钱兑换 II
虽然是按照0-1背包的遍历顺序直接蒙出来,但也是有动态规划五步法,尤其是第五步的举例模拟相同才去实现代码的。
class Solution {
public int change(int amount, int[] coins) {
int[] dp = new int[amount+1];
dp[0] = 1;
for(int i = 0;i < coins.length;i++){
for(int j = coins[i];j <= amount;j++){
dp[j] += dp[j-coins[i]];
}
}
return dp[amount];
}
}
4.爬楼梯 (进阶)
题目链接:70. 爬楼梯 (进阶)
思路:动态规划五步法
- 是否属于背包问题?
- 目标楼梯数为n则相对于背包重量,每次只能走1层或两层表示物品重量/价值,能重复走→完全背包问题。
- dp[j]表示当背包重量为j时,共有dp[j]个走法→考虑要选择物品顺序→求排序
- 递推公式:dp[j] += dp[j-nums[i]]
- 初始化dp[0] = 1,求排序or组合问题的递推公式为累加,所以初始化必须为1。
- 并且重量为0时,无论走多少层都能达到,所以可设置为初始化为1。
- 遍历顺序:完全背包排序问题→先背包重量正序遍历,再物品正序遍历。
- 举例验证思路正确性(略)
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
for(int j = 1;j <= n;j++){
for(int k = 1;k <= 2 && j >= k;k++){
// if(j >= k){
dp[j] += dp[j-k];
// }
}
}
return dp[n];
}
}
物品for循环中的判断能够加入 j>= k,因为物品重量是从小到大的,所以若j<i则表示其后面的都大不过也就没有遍历的必要。
5.零钱兑换
题目链接:322. 零钱兑换
思路:动态规划五步法
- 是否背包问题?
- 求最小硬币数组成目标值target→背包重量,硬币无限选→物品重量 = 完全背包
- dp[j]表示背包重量为j时,最小硬币组成目标值数dp[j]
- 递推公式:dp[j] = Math.min(dp[j],dp[j-nums[i]]+1);
- 初始化:背包重量为0时,不需要硬币就能组成,所以dp[j] = 0?
- 遍历顺序:求最小数→完全背包不需要考虑for循环内外顺序,反正都是通过计算覆盖。
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
dp[0] = 0;
for(int j = 1;j <= amount;j++){
dp[j] = amount+1;
}
for(int i = 0;i < coins.length;i++){
for(int j = coins[i];j <= amount;j++){
dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);
}
}
return dp[amount] == amount+1?-1:dp[amount];
}
}
采用求排序的顺序遍历:外层物品,内层背包重量都是正序。
剪枝:内层背包令j从coins[i]开始,因为小于该值都不能更新dp[j]。
6.完全平方数
题目链接:279.完全平方数
思路同零钱兑换+物品遍历for(int i = 0;i*i <= n;i++)
7. 单词拆分
题目链接:139.单词拆分
思路: 动态规划六步法
1.是否是背包问题?
单词重复使用→物品+可能用完全背包;目标值为字符串列表→背包重量
2.dp[j]当背包重量为j时,是否能够拼接。
3.递推公式:如果当下的能够部分拼接,若删除该段先前也能拼接则能够拼接返回true
if(s.substring(j-wordDictLen,j).equals(wordDict.get(i))){
dp[j] = dp[j-wordDictLen];}
4.初始化:刚开始背包重量为0,肯定能拼接,所以默认dp[0] = true
5.遍历顺序:单词拼接有顺序→排序问题→for外背包重量,内物品。
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int slen = s.length();
boolean[] dp = new boolean[slen+1];
dp[0] = true;
int wordDictLen;
for(int j = 1;j <= slen;j++){
for(int i = 0;i < wordDict.size();i++){
wordDictLen = wordDict.get(i).length();
if(j >= wordDictLen && dp[j] == false){
if(s.substring(j-wordDictLen,j).equals(wordDict.get(i))){
dp[j] = dp[j-wordDictLen];
}
}
}
}
return dp[slen];
}
}
递推公式判断后赋值写成:dp[j] = dp[j-wordDictLen]会需要内嵌dp[j] == false,并若break,
否则可能会导致dp[j]本来赋值true后被后面也满足判断的dp[j-wordDictLen] = false覆盖。
代码优化:使用哈希判断是否存在
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()];
}
}