文章目录
01背包与完全背包
动态规划背包问题有两个关键要素:物品、背包;
涉及的问题类型
01背包(每个物品用一次):
- 给定容量背包最大能装的物品容量。(分割等和,两组数的和相差最小值)
- 给定容量背包最多能装的物品个数。
完全背包(每个物品用多次):
- 求组合数,即物品不要求顺序。
- 求排列数,即物品有顺序要求。
背包问题的方法论(个人总结)
对于背包遍历顺序;由于大多时候使用的是一维背包,涉及次轮遍历与上一轮遍历的状态关系;因此在对背包进行遍历的时候有要求,判断逻辑为物品是否可以重复使用。主要是由于,dp[i]由dp[i-nums[i]]推导得来,从左向右,则对物品会使用多次。
- 01背包←:从右向左遍历背包。
- 完全背包→:从左向右遍历背包。
对于物品和背包的遍历顺序;判断逻辑为求组合数还是排列数。我的理解为是,先遍历物品,物品是依照顺序进行遍历的,对于一个背包来说,不会出现添加物品1,添加物品2,又添加物品1的情况,因此适用于组合数。而后遍历物品,则有可能会出现这种121的情况。
- 组合数:先物品。(后背包)
- 排列数:后物品。(先背包)
tips: 一般而言,大多数情况是先物品,后背包。涉及完全背包,排列数时,要对上述两个方面进行考虑。
例题
LeetCode.416.分割等和子集
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for(int i = 0; i < nums.length; i++) {
sum += nums[i];
}
if (sum % 2 == 1) return false;
else sum = sum / 2;
int[] dp = new int[sum + 1];
// dp[i] 表示容量为i的背包能装的最大重量
dp[0] = 0;
for (int i = 0; i < nums.length; i++) { // 遍历物品
for (int j = sum; j >= nums[i]; j--) { // 遍历背包,从右向左
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[sum] == sum ? true : false;
}
}
LeetCode.322.零钱兑换(最少数量)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4s6GsNLw-1660631453606)(%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%9201%E8%83%8C%E5%8C%85%E4%B8%8E%E5%AE%8C%E5%85%A8%E8%83%8C%E5%8C%85.assets/image-20220816142331878.png)]
class Solution {
public int coinChange(int[] coins, int amount) {
// 完全背包,从左向右
// 组合or排列都可以
int[] dp = new int[amount + 1]; // 凑成金额i的最少硬币数为dp[i]
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount; j++) {
if (dp[j - coins[i]] != Integer.MAX_VALUE)
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
}
LeetCode.377.组合总和IV
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1]; // dp[i] 表示总和为i的组合的个数
dp[0] = 1;
// for (int i = 0; i < nums.length; i++) {
// //先遍历物品,物品按顺序遍历,为组合数
// for (int j = nums[i]; j <= target; j++) {
// //遍历背包,可以重复,左到右遍历
// dp[j] = dp[j] + dp[j - nums[i]];
// }
// return dp[target];
for (int j = 1; j <= target; j++) {
//遍历背包,可以重复,从左到右
for (int i = 0; i < nums.length; i++) {
// 后遍历物品,物品顺序是可变的,排列数
if (j >= nums[i]) {
dp[j] += dp[j - nums[i]];
}
}
}
return dp[target];
}
}
LeetCode.474.一和零(二维背包)
使用未压缩的二维数组动态规划,不涉及从左还是从右的考虑,因为两轮遍历过程中,涉及状态的复制,不涉及状态的覆盖。
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][][] dp = new int[strs.length + 1][m + 1][n + 1]; // m个0,n个1的最大数量
for (int b = 1; b <= strs.length; b++) {
String str = strs[b - 1];
int mm = 0, nn = 0;
for (int k = 0; k < str.length(); k++){
if (str.charAt(k) == '0') mm++;
else nn++;
}
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i >= mm && j >= nn) {
dp[b][i][j] = Math.max(dp[b - 1][i][j], dp[b - 1][i - mm][j - nn] + 1);
} else {
dp[b][i][j] = dp[b - 1][i][j];
}
}
}
}
return dp[strs.length][m][n];
}
}
使用压缩的滚动数组动态规划,需要从右向左进行遍历。
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1]; // m个0,n个1的最大数量
for (int b = 1; b <= strs.length; b++) {
String str = strs[b - 1];
int mm = 0, nn = 0;
for (int k = 0; k < str.length(); k++){
if (str.charAt(k) == '0') mm++;
else nn++;
}
for (int i = m; i >= mm; i--) {
for (int j = n; j >= nn; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i - mm][j - nn] + 1);
// 二维背包的滚动数组,从后向前。
}
}
}
return dp[m][n];
}
}
参考:《代码随想录》,https://programmercarl.com