01背包基础知识
(参考卡哥内容)
01背包问题:
有n件物品和一个最多能背重量为w的背包。第i件物品的重量是weight[i],得到的价值是value[i]。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
两种解法:
二维数组和一位滚动数组
二维dp数组
动态规划五部曲分析:
1.确定dp数组以及下标的含义
dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
要时刻记着dp数组的含义,下面的一些步骤都围绕这dp数组的含义进行的
2.递推公式
- 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
- 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
3.初始化
dp[i][0] = 0;(默认初始化,不需要进行操作)
dp[0][j] 在j >= weight[0]的时候,dp[0][j] = value[0];
4.遍历顺序
从定义和递推公式出发
从上到下,从左到右
5.举例推导:略
以上推导,i与j的顺序默认 i 遍历的是物品,j遍历的是重量。
这两者可以颠倒,与此同时,定义初始化等都要跟着相应地变化(一定要注意,思维的统一)
一维滚动dp数组
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。
还是按照动态规划五部曲来进行讲解:
1.dp数组的含义
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
2.递推公式
dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。
dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])
此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,
所以递归公式为:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
3.初始化
dp[0] = 0;
4.遍历顺序(重要)
for (int i = 0; i < wLen; i++){
for (int j = bagWeight; j >= weight[i]; j--){
}
}
内外循环不能变,而且内部循环,重量要采用倒序
【原因:倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!】
【再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?
不可以!因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。】
原因方面:倒序在前,两个嵌套for循环不能颠倒的原因在后。
一、分割等和子集
链接: 416.分割等和子集
思路
本题属于01背包问题的应用
自己有点自大且忽略了很多点
【忽然明白为什么要每天都要做几道算法题了,要保持的是什么思维。隔一天不做算法题,自己只会最直白最简单的思路,所以每天必须要做几道算法题】
分析题意:
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等
分成两份,且元素和相等,不就是 sum / 2
很自然联想到,如果 总和为奇数,则return false
转化为背包问题
背包的容量为sum / 2
每个元素的重量和价值相同均为 nums[i]
每个元素只能取一次
运用一位滚动数组
1.dp数组的含义
dp[j]容量为j的背包中装入的最大和是多少
2.递推公式
直接套入01背包公式
dp[j] = Math.max(dp[j],dp[j - nums[i]] + nums[i]);
3.初始化
dp[0] = 0
4.遍历顺序
(二维数组不需要考虑很多,背包容量和物品的遍历顺序也可以颠倒)但是一维滚动数组需要考虑很多东西,不能上来就做
4.1 首先是定义的dp[j]的范围
int[] dp = new int[target + 1];
4.2 遍历背包容量和物品的顺序
物品在前面,背包在后面
4.3 j 的范围,从 target开始 倒序 – 到 nums[i]
5.举例推导
代码
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;
int target = sum / 2;
int[] dp = new int[target + 1];
for (int i = 0;i < nums.length;i++){
for (int j = target;j >= nums[i];j--){
dp[j] = Math.max(dp[j],dp[j - nums[i]] + nums[i]);
}
}
return dp[target] == target;
}
}
二、最后一块石头的重量II
链接: 1049.最后一块石头的重量II
思路
本题尽可能让石头分成重量相同的两堆,这样相撞之后剩下的石头最小
与分割等和子集类似,都可以转化为01背包问题
区别在于分割等和子集,如果数组之和为奇数,则是错误的
而本题即使是奇数,我们可以继续进行操作
一堆石头的重量是:dp[target] target = sum / 2
【不是target,而是dp[target],target是我们计算的数值,dp[target]才是容量为target的容器可以装的最大重量】
另一堆石头的重量是:sum - dp[target]
sum - dp[target] >= dp[target]
所以最后结果是sum - 2 * dp[target]
注意:有时候背包问题的dp[j]不一定和题目直接相关
代码
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int i = 0;i < stones.length;i++){
sum += stones[i];
}
int target = sum / 2;
int[] dp = new int[target + 1];
for (int i = 0;i < stones.length;i++){
for (int j = target;j >= stones[i];j--){
dp[j] = Math.max(dp[j],dp[j - stones[i]] + stones[i]);
}
}
return sum - 2 * dp[target];
}
}
三、目标和
链接: 494.目标和
思路
感觉本题其实是小学难度的问题
但是换成算法自己就迷糊了
本题 left 为正数之和,right 为负数之和(不加负号)
left + right = sum
left - right = target
从而求出left = (sum + target) / 2
如果无法整除则return 0
注意如果sum < abs(target) 直接return 0
接着就转化为正常的01背包问题
本题有些不同的是求种类,这与我们爬楼梯问题有些类似
得到的递推公式是:
dp[j] += dp[j - nums[i]];
种类相加 像爬楼梯中dp[j - 1] + dp[j - 2]
代码
class Solution {
public int findTargetSumWays(int[] nums, int target) {
// 存在负数的情况
int sum = 0;
for (int i = 0;i < nums.length;i++){
sum += nums[i];
}
if ( sum < Math.abs(target)) return 0;
if ((sum + target) % 2 == 1) return 0;
// 背包容量为(sum + target) / 2
int size = (sum + target) / 2;
if (size < 0) size = -size;
int[] dp = new int[size + 1];
dp[0] = 1;
for (int i = 0;i < nums.length;i++){
for (int j = size;j >= nums[i];j--){
dp[j] += dp[j - nums[i]];
}
}
return dp[size];
}
}
四、一和零
链接: 474.一和零
思路
本题思路其实比较简单,只是代码结构看起来有些让人畏惧
本题因为有 0 和 1,所以是两个维度,设成二维数组
遍历字符串数组,求出字符串数组中各元素中含有多少0和多少个1
不必遍历一遍字符串后在进行其他操作,可以边遍历边进行背包处理
for (int i = m; i >= x;i--){
for (int j = n;j >= y;j--){
dp[i][j] = Math.max(dp[i][j],dp[i - x][j - y] + 1);
}
}
i,j在这里都是背包容量的存在,因此要倒序
递推公式由题目可以看出是求数量
dp[i][j] = Math.max(dp[i][j],dp[i - x][j - y] + 1);
有脑筋急转弯的成分。
代码
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1];
for (String str : strs){
int x = 0;
int y = 0;
for (int i = 0;i < str.length();i++){
if (str.charAt(i) == '0') x++;
if (str.charAt(i) == '1') y++;
}
// 背包容量
for (int i = m; i >= x;i--){
for (int j = n;j >= y;j--){
dp[i][j] = Math.max(dp[i][j],dp[i - x][j - y] + 1);
}
}
}
return dp[m][n];
}
}