本题就和 昨天的 416. 分割等和子集 很像了,可以尝试先自己思考做一做。
视频讲解:https://www.bilibili.com/video/BV14M411C7oV
https://programmercarl.com/1049.%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E7%9A%84%E9%87%8D%E9%87%8FII.html
哎卧槽太难了。这次还要一次只选2个,我不懂啊。看题解吧
我觉得背包问题,透过题目看到背包就是最难的。
他说两块石头相撞,撞出一个差值。再拿两块装……这个过程感觉很复杂,而且和背包没关系
但是最后就是撞没或者撞得就剩一个。
其实就是把所有石头分成两堆,尽可能的平均的两堆。这样撞出来的结果就是最小值。
想到这我产生一个疑问: 比如总重23,分成11 和 12,但是石头一定能凑出11么?不一定啊,万一是8 和 15这样子的呢?为什么就是target = 11 去做背包问题啊?
再细想一下: 其实不是石头重量凑出11,而是准备一个总容量11的背包,看这个背包能装的石头(往里装是属性A)最大价值(在这道题就是属性B的重量)是多少。
因为最理想的情况就是容量11的包装重量11的石头,这样结果就是 1,就是最小的了。
这么想的话,就是很合理的背包问题了啊,就从题目看到背包了。
就可以写出以下代码了。
//求出所有石头的总重
int totalWeight;
for (int i = 0; i < stones.length; i++) {
totalWeight += stones[i];
}
//题干的最小情况时,背包的大小target应该是总重一半,看这个包装的最大价值石头是多少
int target = totalWeight / 2;
和分割相等子集一样,背包容量是 j 的时候,dp[j] 代表 j容量的背包能装的石头的最大属性B价值(本题也是重量)。
//dp数组的大小应该是背包的容量 + 1
int[] dp = new int[target + 1];
和分割相等子集一样,dp[0] = 0,其余元素应该初始化称最小的非负数。其实也就不用初始化了。
背包问题的递推公式是
dp[j] = max( dp[j], dp[ j - weight[I] ] + value[I] );
在本题种weight 和value 都是stones[] 。
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
那么就可以写出下面的代码:
//递推公式
for (int i = 0; i < stones.length; i++) {
//背包容量能装进这个石头,才需要计算,否则保持滚动前的数据就可以了
for (int j = target; j >= stones[i]; j--) {
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
其中内层 for 循环的终止条件写成 j >= stones[i];
更方便。
不然就需要 写一个 if条件进行判断,而且 else 里还不用写东西。这样不够高雅了。
for (int j = target; j >= 1; j--) {
if (j >= nums[i]) {
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
懒得说了
无
我越来越理解我的 属性A B问题了。
在分割子集的时候我觉得怎么看背包装不装满有点晦涩难懂,到这里看的是背包最多装多少,我一下就明白了。
能不能装满就是看最多呀,最多装多少,能不能装满
我太牛了。
但更难的难在,怎么看出是背包问题吧
class Solution {
public int lastStoneWeightII(int[] stones) {
//求出所有石头的总重
int totalWeight = 0;
for (int i = 0; i < stones.length; i++) {
totalWeight += stones[i];
}
//题干的最小情况时,背包的大小target应该是总重一半,看这个包装的最大价值石头是多少
int target = totalWeight / 2;
//dp数组的大小应该是背包的容量 + 1
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 totalWeight - dp[target] - dp[target];
}
}
大家重点理解 递推公式:dp[j] += dp[j - nums[i]],这个公式后面的提问 我们还会用到。
视频讲解:https://www.bilibili.com/video/BV1o8411j73x
https://programmercarl.com/0494.%E7%9B%AE%E6%A0%87%E5%92%8C.html
乍一看挺组合的,哪里背包了啊???我想想
比如题里的例子 1 1 1 1 1 ,目标凑出 3. 所有数字的和是5,那么我的背包就是 5-3 = 2.
也就是说我只要找出哪些数字能凑出2,前面加负号就行。就是背包总容量是 2. 数字的重量和价值也是一样的。
但是这道题不再是,能不能凑出来?而是肯定能凑出来,有几种的问题。这我也不太明白了
看看题解吧。
我上面的思路是错的,我给sum当做正数部分了,当有两个1取负,负数部分 -2, 正数部分就是 3 了。 结果就是1 而不是目标 3了。
正确的应该是,正数部分up,负数部分down :
up + down = sum = 5
up - down = target = 3
那么up =( sum + target) / 2 = 4才对
也就是背包大小是这个 up,看哪些数字加起来能凑够这个 4。那如果不能整除呢?那么就没有这样的答案,返回0了。
这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。
本题则是装满有几种方法。其实这就是一个组合问题了。
dp[j] 代表 装满容量为 j 的背包,有dp[j] 种方法。这里注意,是装满 j,而之前的事往 j 装,属性B 最大是 dp[j],是不一定装满的。
dp[j] += dp[j - nums[i]]
怎么理解呢。
只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。
比如外层for循环我拿到的nums[i],我想知道j = 5 的时候,也就是背包大小为5,有多少种方法。
那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。
为啥啊??? 为什么要加起来啊?,我拿过来一个数字,这个数字是确定的啊,直接不就是dp[j - nums[i]] 吗,为什么要累加啊?
答案:
我可以从二维dp数组的角度去理解这个累加,但我不太理解一维数组,卡哥讲的那种累加。
去画这个二维数组的过程中就感受到累加的意义了。
比如放入第三个物品(第三个1,图里的1.3)时,想要装满容量为 2的背包(j = 2)有多少种方法?
我拿到了第三个物品,重量是1. 此时第三个物品有两个状态,放入背包和不放入背包。
如果放入背包,就类似上楼梯的问题(联想一下),这个物品已经确定下来了,剩下的背包空间是 1。 问题转换成了,用前两个物品来放,放满容量为 1 的背包有多少种可能? 也就是dp[2 - 1] = dp[1]
如果没有放入背包, 问题转换成了,用前两个物品来放,放满容量为 2 的背包有多少种可能? 也就是dp[2]
那么这三个物品放满容量为 2 的背包有多少种可能呢?就是
dp[2] = dp[2 - 1] + dp[2],dp[2] += dp[2 - 1]
也就是我图里写的,3个1凑2有多少种?2个1 凑1种 + 2个1凑2种
这个例子全都是 1 ,让人感觉有点不和谐。
dp[j] += dp[j - nums[i]]
而是在滚动更新的过程中,我拿到第i个数,装满容量为 j 的包。依赖于用第i个数的种数和不用它的种数之和,用它就是dp[j - nums[i]],不用就是dp[j]
对比一下最大价值那种,滚动更新的过程中,我拿到第i个物品,装进容量为j的包。包内的最大价值 依赖于 用它装的价值 和 不用他装的价值,更大的那个。所以是 max()。
要注意特殊情况,除了up没有被整除,还有一种情况不可以。
因为nums数组都是非负的,想在数组里凑出 up 这大的背包,那么up一定非负。
所以
//如果不能整除就找不到
if ((target + sum) % 2 != 0) return 0;
//sum 一定是正数,如果target绝对值大于了sum绝对值,不可能
if (Math.abs(target) > sum) return 0;
//背包大小 up, 在nums里找数组 凑出up,所以up一定得非负,因为数组元素非负,凑不出负数的。
int up = (target + sum) / 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 ((target + sum) % 2 != 0) return 0;
//sum 一定是正数,如果target绝对值大于了sum绝对值,不可能
if (Math.abs(target) > sum) return 0;
//背包大小 up, 在nums里找数组 凑出up,所以up一定得非负,因为数组元素非负,凑不出负数的。
int up = (target + sum) / 2;
int[] dp = new int[up + 1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) {
for (int j = up; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[up];
}
}
通过这道题目,大家先粗略了解, 01背包,完全背包,多重背包的区别,不过不用细扣,因为后面 对于 完全背包,多重背包 还有单独讲解。
视频讲解:https://www.bilibili.com/video/BV1rW4y1x7ZQ
https://programmercarl.com/0474.%E4%B8%80%E5%92%8C%E9%9B%B6.html
好的,我直接看题解粗略了解8
这道题的背包是两个维度的,也就是有两个属性A,m和n。
所以需要一个二维数组dp[][]
含义是 容量为 i 个0, j 个 1的背包,最多有dp[i][j] 个物品
最终求dp[m][n]
0-1 背包的时候:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
现在维度上升了,那就是 dp[i - x][j - y] + 1,
为什么是 + 1呢。因为求的最多多少个物品,所以就 + 1就行了。
同样,装进这个物品和不装进物品的数量 取最大值、
就是
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。
这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。
和0-1背包一样,就是0
还是倒序
代码上不难
这道题就是纯粹0-1问题的 2维版本
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
// m是0的个数,n是1的个数
int [][] dp = new int[m + 1][n + 1];
for (int i = 0; i < strs.length; i++) {
int numOfZero = 0, numbOfOne = 0;
String str = strs[i];
//统计这个物品的两个重量
for (int k = 0; k < str.length(); k++) {
if (str.charAt(k) == '0') {
numOfZero++;
} else {
numbOfOne++;
}
}
for (int j = m; j >= numOfZero; j--) {
for (int k = n; k >= numbOfOne; k--) {
dp[j][k] = Math.max(dp[j - numOfZero][k - numbOfOne] + 1, dp[j][k]);
}
}
}
return dp[m][n];
}
}