这一期三道题目是对于01背包不同层次的应用,各有特色,也各有难点。同时也是01背包的最后一期,下一期我们来学习完全背包。
1049. 最后一块石头的重量 II - 力扣(LeetCode)https://leetcode.cn/problems/last-stone-weight-ii/最后一块石头的重量II,这道题是将各个不同重量的石头相互碰撞,碰撞规则是如果两石头重量一样,那么两块石头均会撞碎,不会有剩余石头。如果其中一颗石头重量较大,那么会剩下较大重量石头减去较轻石头的重量,也就是说剩下一个新重量的石头,题目要求我们返回尽量小的剩余石头重量。
那么如何尽量使剩余的石头的重量最小呢?我们要做的就是将石头重量大致的平分成两堆,让两堆石头相撞,如果两堆石头总重量相等,那么剩余重量为0,如果不等,那么剩余的重量仍然为最小。这就是我们解题的一个思路。
动规五部曲的分析:
dp数组的含义:当前能承载的重量下能够存储的石头最大重量,我们假设将这一堆石头重量平分为两堆,所以最大的重量容量也就是总石头重量的一半。
递推公式:递推公式也就是借用01背包的模型,将该块石头放入背包或者不放入背包共两种不同的状态。和01背包的递推公式完全一样,这里的重量和价值均为石头的重量。
dp数组初始化:dp【0】初始化为0,因为能装总重量为0的背包,装不进石头。剩下的不为0的背包初始化为什么呢?根据之前的01背包的一维数组的初始化可知,依然初始化为0,因为我们要比较dp【j】的本身,如果初始化过大,就会导致本来应该赋值的量并不能赋值上。不懂得可以去看01背包的详解。
遍历顺序:遍历顺序就是正常的从前遍历石头,先遍历那一颗石头并不影响结果,背包是从后向前遍历的,不懂依然是往前翻01背包的一维数组解决方法,大多数例题我们都用一维数组解决。
打印:在未得到理想的答案时候,我们要使用打印dp数组的方法来辅助分析。
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
vector<int>dp(1501,0);
dp[0]=0;int sum=0;
for(auto i:stones)sum+=i;
int target=sum/2;
for(int i=0;i<stones.size();i++){
for(int j=target;j>=stones[i];j--)
dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
}
return sum-dp[target]-dp[target];
}
};
和我们上面的分析相同的思路,也就是说如果五部曲能完全懂,那么代码也十分容易,数组我们可以给定1501的大小,这是根据题意所得到的,+1是为了避免数组越界。
我们使用的方法是把全部重量的石头分为两半,最大的背包就是其总和一半,最后我们返回的是两堆石头碰撞剩余的部分,总和减去一半再减一半,有人会问了为什么要这样写呢?因为如果石头总重量不能均分两半,那么一定是会有非0的剩余,由于总和/2是向下取整所以一定是取得较小的数,我们这样做减法得到的就是剩余的新石头的重量。
494. 目标和 - 力扣(LeetCode)https://leetcode.cn/problems/target-sum/ 目标和这道题我一看就丝毫没有思路,它有一点像是用回溯算法解决的题目,给定数组的每一个数都有取正和取负两种,然后最后算出共可以有多少次不同的方法凑得目标值。
但事实上是有可以用上01背包的思路的!思路有些不好想,具体思路是将要变成正数的部分和要变成负数的部分分开,正数部分加上那些应该是负数的部分,就可以得到目标值了。那么关键的来了,如何知道我们的左边正数部分应该取多少呢?它为多少时候我们才能加上右边部分凑成target呢?有以下公式推出,left-right=target这也就是说左边数字和右边数字相加就是目标值,为什么是减去呢?因为我们只是将右边看成是负数,其实并不是负数,我们将数组中正数逐个变成负数再放进负数堆里是很麻烦的,不是明智之举,所有我们直接减掉右边的正数就可以达成目的了。而left+right=sum也就是数组本身的数字逐个相加起来,那肯定就是总和sum了。这就可以推出right=sum-left,将该等式回带第一个式子得left=(sum+target)/2。这是个重要的式子,它帮助我们确定左边部分到底放多大,也就是背包最大容量是多大!sum可以求target是题目给的。
dp数组的含义:填满容量为j的背包共有多少种填充方法。
递推公式:递推公式的分析略显复杂,若j=5的情况下,给定数组{1,2,3,4,5},那么我们如果已经加入一个1,则需要dp【4】种方法凑成容量为5的背包,如果已经加入的数字是2,那么则需要dp【3】种方法才能够凑成容量为5的背包,以此类推如果加入的是数字5那么凑成容量为5背包需要dp【0】种,那么我们要求构成dp【5】共多少方法,也就是容量为5共多少种方法,那一定是前面的几种加在一起的和,则为构建的dp【5】的方法。就是dp【j-nums【i】】
而由于是累加在一起故递推公式是dp【j】+=dp【j-nums【i】】。
dp数组初始化:j为0时候初始化为1,这一点是和其他01背包题目初始化不同的地方,我们可以理解为目标值为0则有一种方法,不放入数组元素。事实上经过调试我们也可以知道,如果第一个位置初始化为0,那么累加的一直就是0了,无论target为多少,都无法出来其他数字了,所以一定要初始化为1。而其他j不等于0时候给它们初始化为0,这样方便第一次累加,实际上这和遍历顺序也有关联。
遍历顺序:遍历顺序是从左向右遍历物品,从后向前遍历背包,不懂看之前的文章。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum=0;
for(auto i:nums)sum+=i;
if((target+sum)%2==1)return 0;
int left=(target+sum)/2;
if(abs(target)>sum)return 0;
vector<int>dp(10001,0);dp[0]=1;
for(int i=0;i<nums.size();i++){
for(int j=left;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[left];
}
};
最后我们将dp【left】返回即可知道,我们为了凑成target共能有多少种方法。
474. 一和零 - 力扣(LeetCode)https://leetcode.cn/problems/ones-and-zeroes/这道题又是01背包的一种新的应用题型。这道题乍一看也不像是能用到01背包特性的。这道题有两个维度m和n控制着结果的产生,这意味着我们必须使用二维数组来表示,这和其他题目用一维数组是一样的,其他题目是用一维数组代替二维数组,这道题实际上使用二维数组代替三维数组。
dp数组及其含义:dp【i】【j】i个0j个1的最大子集大小为多少,该背包的最大容量是dp【m】【n】。
递推公式:递推公式是和一维数组的递推公式很类似,dp【i】【j】=max(dp【i】【j】,dp【i-x】【j-y】+1)。其中的x和y分别代表遍历当前物品时候,物品所含有的0数量和1数量。当当前背包的i和j对应的小于了x或者y中的任何一个,则跳出。
dp数组的初始化:初始化全都是0,第一个位置初始化,0个0,0个1装入不了任何的子集。
遍历顺序:仍然是物品从左到右依次遍历,这里我们是一个一个物品取,并且再嵌套一个循环来分解该物品有多少个0和多少个1。分解完毕了,我们走遍历背包,遍历背包依然是从后向前,并且这道题的二维背包,无论是先遍历0还是先遍历1效果都是一样的,没有先后之分。
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>>dp(m+1,vector<int>(n+1,0));
for(string str:strs){
int x=0,y=0;
for(char ch:str){
if(ch=='0')x++;
else y++;}
for(int i=m;i>=x;i--){
for(int j=n;j>=y;j--)
dp[i][j]=max(dp[i][j],dp[i-x][j-y]+1);
}
}
return dp[m][n];
}
};
相当于持续的更新dp【m】【n】,遍历不同的物品,在最大背包的容量之内,我们尽可能地装入更多的物品,也就是拥有给定数组尽可能多的数据(子集)。这里还要对递推公式做进一步的解释,就是为什么我们dp【i-x】【j-y】之后还要有一个+1的操作?我们初始化本来都是0,这里递推公式就是比较当前容量最大应该是当前所包含子集多,还是倒退一次再装入的多,每次进行第二个运算都要+1,意义是加入一个子集,所以我们要使自己数量+1,这和dp数组的定义也是密切相关的,数组就表示了当前状态下的最大子集数量。
说了这么多,。我们来做一个总结,对于01背包这些题型的总结,毕竟已经到了01背包的习题最后一期了。
我们之前做的最开始01背包,是在当前的背包最大容量内能够携带的最大价值是多少。
分割等和子集这道题是给定背包容量,看能不能装满背包,如果不能装满说明不能分割成等和子集。
最后一块石头的重量II是给定背包容量,尽可能装,看能装多少,最后一做差,其实和分割等和子集有一点相像。
目标和是给定背包容量看有多少种装满的方法。
一和零是一个二维背包,给定背包容量看最多能装多少不同的物品。
这些都是01背包的不同维度上的解决问题的典例。其中以后两道题是有一些难度的。
请大家将以上的问题都好好体会一下,以上代码均可ac,如果有不明白01背包的具体实现模板,请参考上一期的作品,里面有针对于一维数组实现和二维数组的实现具体的讲解。