Day37--动态规划--52. 携带研究材料(卡码网),518. 零钱兑换 II,377. 组合总和 Ⅳ,57. 爬楼梯(卡码网)
Day37–动态规划–52. 携带研究材料(卡码网),518. 零钱兑换 II,377. 组合总和 Ⅳ,57. 爬楼梯(卡码网)
本文全部都是 ” 完全背包 “ 问题,从零到入坑,从入坑到爬出来。
本文的精华在:《为什么是“先遍历背包,后遍历数字”?》非常详细地讲解了,为什么先遍历数字,后遍历背包——是组合数。先遍历背包,后遍历数字——是排列数。
52. 携带研究材料(卡码网)
思路:
- 确定dp数组含义:
dp[i][j]
表示从下标为[0-i]
的物品,每个物品可以取无限次,放进容量为j
的背包,价值总和最大是多少。 - 确定递推公式:(和01背包相似,所以只讲区别)
- 在01背包中,每个物品只有一个,既然空出物品1,那背包中也不会再有物品1。所以空出来之后,去上一层找,即
dp[i-1][j-weight[i]]
- 而在完全背包中,每个物品可以无限取,即使空出物品1空间重量,那背包中也可能还有物品1。所以空出来之后,要从本层取,即
dp[i][j-weight[i]]
- 得出递推公式:
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
- 在01背包中,每个物品只有一个,既然空出物品1,那背包中也不会再有物品1。所以空出来之后,去上一层找,即
- 初始化:初始化第一行,从背包能放得下weight[0]这个东西开始,往后遍历。能放多少是多少。
- 确定遍历顺序,每个
dp[i][j]
,都要靠左边,上边的值确定数据。所以是从上往下,从左往右。
import java.util.*;public class Main {public static void main(String[] args) {Scanner in = new Scanner(System.in);int n = in.nextInt();int bagSize = in.nextInt();int[] weight = new int[n];int[] value = new int[n];for (int i = 0; i < n; i++) {weight[i] = in.nextInt();value[i] = in.nextInt();}int[][] dp = new int[n][bagSize + 1];// 初始化第一行,从背包能放得下weight[0]这个东西开始,往后遍历for (int j = weight[0]; j <= bagSize; j++) {dp[0][j] = dp[0][j - weight[0]] + value[0];}// 动态规划for (int i = 1; i < n; i++) {for (int j = 1; j <= bagSize; j++) {// 当前背包放不下weight[i],直接等于上一层if (j < weight[i]) {dp[i][j] = dp[i - 1][j];} else {// 这里的递归公式,与01背包不同。要空出weight[i]的时候,看的是本层的dp// 区别在于,这里是dp[i][j - weight[i]],而01背包是:dp[i-1][j - weight[i]]dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);}}}System.out.println(dp[n - 1][bagSize]);}
}
518. 零钱兑换 II
思路:二维dp数组
- 确定
dp[i][j]
含义,从0-i的coins中,任选一个或多个,装满容量为j的背包(注意是装满) - 确定递推公式:
- 注意这里求的是“最大组合数”,而不是“最大价值”,所以是加法(情况一+二)
- 情况一,不放coins[i],就是
dp[i-1][j]
,情况二,放coins[i],就是dp[i][j-coins[i]]
- 注意这里不是[i-1]了,是[i]。不是看上层的,而是看本层的,这是完全背包和01背包的关键区别
- 初始化
- 初始化第一列:都有“不选”的选择,所以是1
- 初始化第一行:要能取模的才能赋值1,比如最小货币是2的话,当j==3,是不能装满的
// 确定dp[i][j]含义,从0-i的coins中,任选一个或多个,装满容量为j的背包
class Solution {public int change(int amount, int[] coins) {// amount 是 bagSizeint n = coins.length;int[][] dp = new int[n][amount + 1];// 初始化第一列for (int i = 0; i < n; i++) {// 都有“不选”的选择,所以是1dp[i][0] = 1;}// 初始化第一行for (int j = coins[0]; j <= amount; j++) {// 特别注意,要能取模的才能赋值1,比如最小货币是2的话,当j==3,是不能装满的if (j % coins[0] == 0) {dp[0][j] = 1;}}// 动态规划for (int i = 1; i < n; i++) {for (int j = 1; j <= amount; j++) {// 当前硬币大于背包容量,直接等于上一层if (j < coins[i]) {dp[i][j] = dp[i - 1][j];} else {// 注意这里求的是“最大组合数”,而不是“最大价值”,所以是加法(情况一+二)// 情况一,不放coins[i],就是dp[i-1][j]// 情况二,放coins[i],就是dp[i][j-coins[i]]// 注意这里不是[i-1]了,是[i]。不是看上层的,而是看本层的,这是完全背包和01背包的关键区别dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];}}}return dp[n - 1][amount];}
}
思路:一维dp数组
dp[i][j]
含义不变- 递推公式:
- 本题 二维dp 递推公式:
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]]
(正上方,左方) - 压缩成一维:
dp[j] = dp[j] + dp[j - coins[i]]
- 本题 二维dp 递推公式:
- 初始化:dp[0] = 1
- 遍历顺序
- 在01背包的时候,遍历j是要倒序遍历的,因为dp[j]要依靠“正上方”和“左上方”的数据。但是完全背包,它依赖的是:“正上方”和“左方”的数据。
- “左方”的数据由何而来?得先遍历才有,所以是for(j)是顺序遍历
附:01背包时候的二维公式和一维公式
二维:
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - coins[i]]
(正上方+左上方)一维:
dp[j] = dp[j] + dp[j - nums[i]];
因为要依赖左上方的数据,也就是上一层的旧数据,所以只能倒序遍历。遍历右的时候能取到左的数据
// 确定dp[i][j]含义,从0-i的coins中,任选一个或多个,装满容量为j的背包
class Solution {public int change(int amount, int[] coins) {// amount 是 bagSizeint n = coins.length;int[] dp = new int[amount + 1];// 初始化dp[0] = 1;// 动态规划for (int i = 0; i < n; i++) {for (int j = coins[i]; j <= amount; j++) {dp[j] = dp[j] + dp[j - coins[i]];}}return dp[amount];}
}
377. 组合总和 Ⅳ
写在前面:这个题目很有迷惑性,说是“组合”总和,但是这个“组合”又是可以有不同序列的——其实就是“排列”数,而不是求“组合”数。一定要弄清楚这个再继续。
思路:一维dp数组
dp[j]
的定义:从[0-i](可重复)取任意个数,填满背包j的组合(可排列!)有多少种?- 确定递归公式:
dp[j] = dp[j] + dp[j - nums[i]];
(和上题一样,不再详细分析) - 初始化:dp[0] = 1;
- 遍历顺序:先遍历背包,再遍历数字!!!先遍历背包,再遍历数字!!!先遍历背包,再遍历数字!!!
// 一维dp数组
// dp[j]的定义:从[0-i](可重复)取任意个数,填满背包j的组合(可排列!)有多少种?
class Solution {public int combinationSum4(int[] nums, int target) {int n = nums.length;// bagSize 是 targetint[] dp = new int[target + 1];// 初始化dp[0] = 1;// 动态规划for (int j = 0; j <= target; j++) {for (int i = 0; i < n; i++) {if (j >= nums[i]) {dp[j] = dp[j] + dp[j - nums[i]];}}// // 遍历dp检查// for(int k=0;k<=target;k++){// System.out.print(dp[k]+" ");// }// System.out.println(" ");}return dp[target];}
}
为什么是“先遍历背包,后遍历数字”?
先说结论:
先遍历数字,后遍历背包——是组合数。
先遍历背包,后遍历数字——是排列数。
为什么会这样呢?
公式 dp[j] += dp[j - nums[i]]
的核心逻辑是:
要计算 “填满容量 j 的方式数”,可以先放入一个 nums [i],再累加 “填满剩余容量 j-nums [i] 的方式数”。
- 当先遍历数字时,“最后一个元素” 只能是当前数字(按固定顺序),所以不会产生新顺序。
- 当先遍历背包时,“最后一个元素” 可以是任何数字(在同一容量下尝试所有数字),所以会产生不同顺序的排列。
公式本身不关心顺序,只关心 “是否加入当前数字”,而遍历顺序决定了 “加入数字的时机和范围”,最终导致结果差异。
到这里,基本上讲清楚了。可以再把518. 零钱兑换 II按照“先遍历背包,后遍历数字”的方式写一遍,答案肯定错误。
57. 爬楼梯(卡码网)
《代码随想录》:
这其实是一个完全背包问题。
1阶,2阶,… m阶就是物品,楼顶就是背包。
每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。
问跳到楼顶有几种方法其实就是问装满背包有几种方法。
思路:
- 确定dp含义:dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。
- 确定递推公式:
dp[j] += dp[j - i];
- 初始化:dp[0] = 1;
- 完全背包问题:先背包,后数字(因为要先固定背包容量,再取数字,才有不同的顺序)
要是能识别出来是完全背包问题,然后又刚好做完上一题的话,这道题其实可以直接复制过去AC。
import java.util.*;public class Main {public static void main(String[] args) {Scanner in = new Scanner(System.in);int n = in.nextInt();int m = in.nextInt();// bagSize 是 n// coins[] 是 m, [1,2,3...m]// dpint[] dp = new int[n + 1];// 初始化dp[0] = 1;// 先背包,后数字for (int j = 0; j <= n; j++) {for (int i = 1; i <= m; i++) {if (j >= i) {dp[j] += dp[j - i];}}}System.out.println(dp[n]);}
}