代码随想录算法训练营第四十天|01背包 二维 01背包 一维 416.分割等和子集
01背包 二维:
文档讲解:卡码网|46.01背包
视频讲解:https://www.bilibili.com/video/BV1cg411g7Y6
状态:已做出
一、题目要求:
01背包就是有多个物品,每个物品有其自己的价值和重量,每个物品数量只有一个,然后把这些物品放入载重为w的背包里,求出能放入这个背包的物品最大总价值。
二、常见错误:
这道题目最容易出现的错误就是把dp二维数组里的i和j弄错,一旦弄错两者代表的含义就会出现很多的问题,比如初始化的时候把i和j看反了那整个初始化就会弄反。还有一点容易弄错我点,就是在后续的嵌套循环里可能会把物品的价值和重量给弄错,导致后面应该加上物品重量却加上了价值,这里我当时就出错了。
三、解题思路:
这道题目使用动规,这里使用二维数组dp。首先第一步要明白dp[i][j]的含义,要想了解其中的含义,必须要知道物品只有两种情况,一种是将此物品放入背包,另一种是不放,这就是01背包的关键思想, 根据这个思想可以知道,i表示0~i之间任意一个放入背包,j则是背包的重量,dp[i][j]就是[0~i]物品里任意物品放入重量为j的背包里的最大价值。第二步是确定推导公式,根据01背包的特性,每个物品都只有拿与不拿两种状态,所以在遍历到i物品的时候,i物品有两种情况,不拿物品i时dp[i][j]的值就是dp[i-1][j],拿物品i的话,先要把重量j的背包先预留空间给物品i,剩下的背包重量就是j-weight,那么就把value[i]加上dp[i-1][j-weight]的最大价值就可以得到放入物品i的最大价值了。第三步就是初始化dp数组,这里根据推导公式可以知道每个元素都是通过i-1得到的,所以这里必须要对二维数组的第一行进行初始化,那么应该初始化为什么呢?dp这个二维数组行表示物品,列表示背包重量,那么第一行初始化就和地雷零个物品有关,当背包重量不小于背包重量的时候就初始化这个位置元素为value[0],其他的dp元素都初始化为零就行,因为根据推导公式来看,后面的元素都是直接覆盖的,所以其他位置初始化为任何数都行,但第一列所有元素都一定是零,所以所有元素都初始化为零更好。第四步就是确定遍历顺序,这里文档说的很明白,不管是先遍历物品还是先遍历背包重量都是可以的,因为推导公式不会受到遍历方向的影响。最后一步就是按照推导公式举个例子把dp数组元素都推导出来,对比一下代码得出的元素既可。
四、代码实现:
//这里n是背包的最大容量
int dun(vector<int>& wight, vector<int>& value, int n) {int m = value.size(); //将物品个数用变量m来保存//创建二维数组dp,全部元素先初始化为零vector<vector<int>> dp(m, vector<int>(n+1, 0));//将dp数组的第一行所有元素进行初始化,背包容量不小于第零个物品重量的时候将此位置元素赋值为第零个物品的价值for (int j=1; j<=n; ++j) {if (j >= wight[0]) dp[0][j] = value[0];}//开始遍历dp的所有元素,这里i从1开始,因为第零个物品已经初始化for (int i=1; i<m; ++i) {for (int j=1; j<=n; ++j) {if (j >= wight[i]) dp[i][j] = max(dp[i-1][j], dp[i-1][j-wight[i]] + value[i]);else dp[i][j] = dp[i-1][j];}}return dp[m-1][n]; //这里最后返回最后一个物品背包重量为n位置的dp元素,就是得到的答案
}
五、时间复杂度:
时间复杂度:因为是嵌套循环遍历整个二维数组,所以时间复杂度是O(n*m)。
空间复杂度:要创建一个二维数组,所以空间复杂度也是O(n*m)。
六、收获:
通过练习这道经典的01背包,对01背包有了初步的了解,对动规的五个步骤有了更加深入的了解,像这种二维数组的动规,需要对一行或者一列进行初始化,其中遍历循环也需要额外注意,这里二维数组虽然不需要注意遍历循环,但是如果要缩短为一维数组就需要额外注意遍历顺序。
01背包 一维:
文档讲解:卡码网|46.01背包
视频讲解:https://www.bilibili.com/video/BV1BU4y177kY
状态:已做出
一、题目要求:
这次使用一维数组对题目进行求解。
二、常见错误:
这道题目如果使用一维数组dp,在嵌套循环的遍历循环里最容易出现错误, 因为一维数组遍历顺序和二维数组不一样,嵌套的循环必须要从后往前遍历,如果没注意从前向后遍历就会出错。
三、解题思路:
这里一维数组是dp[j],第一步需要了解dp[j]的含义,这个j是背包重量,一维数组是把i给优化掉了,那么dp[j]含义就是背包重量为j的最大物品价值。第二步推出推导公式,按照二维数组的推导公式优化,这里既然把i给去掉了,那么dp[j]就是通过同一行来推出来,如果物品i不放,dp[j]就不用变,如果放物品i那么就是dp[j]=dp[j-weight[i]],因为要给物品i预留空间,剩下的背包重量最大物品价值就是dp[j-weight[i]],这里还需要对比一下两者的最大值,所以最后的推导公式就是dp[j]=max(dp[j],dp[j-weight[i]]+value[i])。第三步就是初始化dp数组,这里统一对dp数组初始化为零,因为dp的元素需要同一行来推出,所以初始化为零才能不影响后续推导。第四步就是确定遍历顺序,这里使用以为数组的话遍历顺序必须要额外注意,首先遍历物品i,再遍历背包重量,这两者不能交换,因为如果先遍历背包重量,dp[j]最后保存的是上一列最后一个物品的最大价值,后面的背包重量就无法推出了。最后举例出一维数组dp的元素和代码得出的对比既可。
四、代码实现:
int dun(vector<int>&wight,vector<int>&value,int n) { //这里n是背包最大容量int m=value.size(); //将物品的个数用变量m来保存vector<int>dp(n+1,0); //创建一维数组dp,先对所有元素都初始化为零//下面这个循环对第零个物品进行初始化,不小于第零个物品重量的背包容量最大价值都初始化为第零个物品的价值for(int i=0;i<=n;++i)if(wight[0]<=i) dp[i]=value[0]; //下面这个循环就是遍历所有物品,遍历的先后顺序不能交换for(int i=1;i<m;++i) { for(int j=n;j>=wight[i];--j) { //这里循环必须要倒序遍历,不然会出现物品重读放入的情况dp[j]=max(dp[j],dp[j-wight[i]]+value[i]); } } return dp[n];}
五、代码复杂度:
时间复杂度:和二维数组的一样,都是O(m*n),因为一维数组也需要使用嵌套循环。
空间复杂度:这里压缩了空间,将二维变为一维,所以空间复杂度是O(m),m是背包的重量。
六、收获:
这次主要是练习01背包的一维数组解法,这种解法能大大减少空间复杂度,学习一维数组的解法也能对遍历循环有新的认识,之前的动规遍历循环的重要性都没体现出来,而这次01背包的一维数组解法就完美的体现出了遍历顺序的重要性,确定好正确的遍历顺序才能解决一维数组dp的赋值。也学会了怎么将二维数组的推导公式转化为一维数组,还有初始化的改变。
416.分割等和子集:
文档讲解:代码随想录|416.分割等和子集
视频讲解:https://www.bilibili.com/video/BV1rt4y1N7jE
状态:已做出
一、题目要求:
题目要求把数组查分为两个部分,这两个部分元素总和必须相同,相同返回true,不同返回false。
二、常见错误:
这道题目我当时做的时候误把整个数组的元素和作为背包容量,导致最后得出错误代码。
三、解题思路:
这道题目难点就是要找出物品和背包,哪个能作为物品哪个能作为背包。根据题目描述,是把一个数组拆分为两个部分,那么数组元素就是物品,那背包又是什么呢?题目要求两个部分元素和相同,那么元素和就是背包的容量,这里是把拆分的子数组作为背包,这样操作就符合01背包问题了。我自己的代码使用的是bool类型的二维dp数组,而文档给出的解法是int型一维数组dp解法,我使用bool是看到题目要求返回bool类型,所以直接就使用了bool型dp。首先第一步就是确定dp[i][j]的含义,这里dp数组的含义就是在[0~i]之间的元素任意放入容量为j的背包,dp[i][j]就是判断是否符合题意,最后直接返回dp[n-1][target]就可以了。第二步就是找出推导公式,这里我使用了bool类型,那么就不需要考虑元素的价值了,之前那个01背包需要加上价值,这里bool就不用考虑元素价值了,如果不拿元素i,那么dp就是dp[i-1][j],如果拿元素i就是dp[i][j-nums[i]],这里dp要等于dp[j-nums[i]]的前提是j不小于nums[i],如果小于直接等于dp[i-1][j]。第三步就是初始化,我这里使用bool类型初始化是最重要的,如果初始化没做好,就全都会出错,这里先把所有二维数组初始化为false,随后只要初始化第一行就可以了,因为背包容量为零的话一定是false,随意不用额外初始化操作,第一行是考虑第零个元素,所以,只要当前背包容量等于第零个元素就初始化为true,其他都是false,因为根据题目的意思,只要背包容量等于拿到的元素和,就符合要求,所以这里要这么设置。第四步是找到遍历顺序,这里使用二维数组,所以遍历顺序都是从前往后,并且两个循环可以交换。最后一步举例既可。
四、代码实现:
class Solution {
public:bool canPartition(vector<int>& nums) {int n=nums.size(); //这里使用变量n来保存物品数量int sum=0;for(int i=0;i<n;++i) sum+=nums[i]; //这里sum来统计数组和if(sum%2!=0) return false; //如果数组和不能整除2就一定不会符合题意int target=sum/2; //这里target作为最大背包容量,根据题意,最大背包容量就是数组和的一半//创建bool类型的二维数组dp,先初始为falsevector<vector<bool>>dp(n,vector<bool>(target+1,false));//这里对第一行进行初始化,只有第零个物品等于背包容量才算符合题意for(int i=0;i<=target;++i) if(nums[0]==i) dp[0][i]=true;//这里是对第一列进行初始化,第一列的数组和为零,那么怎么都符合题意,所以全为truefor(int i=0;i<n;++i) dp[i][0]=true;for(int i=1;i<n;++i) {for(int j=1;j<=target;++j) {//这面就使用推到公式来遍历数组元素if(nums[i]<=j) dp[i][j]=dp[i-1][j] || dp[i-1][j-nums[i]];else dp[i][j]=dp[i-1][j];}}return dp[n-1][target]; //最后直接返回二维数组右下角的元素}
};
五、代码复杂度:
时间复杂度:O(n*m)
空间复杂度:O(n*m)
六、收获:
这道题目就是01背包的应用题,通过找到物品和背包来转化为01背包,这里使用bool类型来解其实我自己觉得挺神奇的,当时一直没想到bool类型的推到公式应该怎么得到,以为bool不行,后面还是问了ai能不能使用bool来解,结果还能用bool解,随后仔细看了推导公式也是终于明白了为什么是这个推导公式,文档给出的解法是一维数组int型dp,更加直观和便捷。通过这道题目的练习了解到了01背包的应用题的解决方式,对这类题有了一定的了解。