理论内容都在代码随想录了,这里我主要是自己写题解回顾,强烈推荐
代码随想录代码随想录PDF,代码随想录百度网盘,代码随想录知识星球,代码随想录八股文PDF,代码随想录刷题路线,代码随想录知识星球八股文https://www.programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html
目录
模板一:二维数组版本
模板二:一维数组版本(滚动数组)
1:分割等和子集(01背包)
2:最后一块石头的重量 II(01背包)
3:目标和(组合问题)
4:一和零
完全背包:
5:零钱兑换||(组合问题)
6:零钱兑换||||(排列总和)
void test_2_wei_bag_problem1() { vector<int> weight = {1, 3, 4}; vector<int> value = {15, 20, 30}; int bagweight = 4; // 二维数组 vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0)); // 初始化 for (int j = weight[0]; j <= bagweight; j++) { dp[0][j] = value[0]; } // weight数组的大小 就是物品个数 for(int i = 1; i < weight.size(); i++) { // 遍历物品 for(int j = 0; j <= bagweight; j++) { // 遍历背包容量 if (j < weight[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); } } cout << dp[weight.size() - 1][bagweight] << endl; } int main() { test_2_wei_bag_problem1(); }
问题:
(1)初始化问题:不能随意的凭感觉的初始化为0,或初始化为1,要根据实际情况
当这个点是初始点时,将数据带入,并检查,因为这个点时所有点的基石
当这个点不为初始点时,一般初始化为0(防止覆盖后面递推得出的数据)
比如在这个模板中:将第一行全部初始化为value[0]
意义为:当背包容量>=1时,如果只放第一件物品,那么背包所得到的最大价值就是value[0]
(2)for循环遍历顺序问题:
一般我们都是先正序遍历物品的重量,再正序遍历背包容量,但实际上,在二维数组中,这个顺序完全可以颠倒,即先正序遍历背包容量,再正序遍历物品的重量;
因为它们之间互相不会覆盖,所以不受影响,但是下面的一维数组就要考虑了
(3)递推公式问题:01背包模板递推公式:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
含义:dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少
返回即返回dp[weight.size()-1][bagweight],也就是递推的最后一步
当不放入物品时:背包容量没有损失,最大价值等于上一个的价值(没有拿,价值不变)
当放入物品时:背包容量=背包容量-物品的重量,最大价值等于上一个的价值+新增物品的价值
#include<vector> #include <stack> #include<queue> #include<iostream> using namespace std; void test() { vector<int> weight = { 1, 3, 4 }; vector<int> value = { 15, 20, 30 }; int bagweight = 4; //核心是:物品重量作为底,将背包的最大容量作为天花板,将天花板依次--,直到正好等于底 //依次统计天花板降低时,dp[j]的最大价值 //之后再重新换一个底,重复上述步骤 vector<int> dp(bagweight + 1, 0); for (int i = 0; i < weight.size(); i++)//物品重量 { for (int j = bagweight; j >= weight[i]; j--)//背包容量 { dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } cout << dp[bagweight] << endl; } int main() { test(); return 0; } dp[4]=max(dp[4]),dp[4-1]+15)=max(dp[4],dp[3]+15) j=4 dp[3]=max(dp[3],dp[4-1]+15)=max(dp[3],dp[2]+15) j=3 dp[2]=max(dp[2],dp[1]+15) j=2 ->> dp[1]=max(dp[1],dp[0]+15) j=1 ->> dp[1]=max(0,15)=15 dp[4]=max(dp[4],dp[4-3]+vaule[1])=max(dp[4],dp[1]+20)=max(0,15+20)=35 dp[3]=max(dp[3],dp[3-3]+value[1])=max(dp[3],dp[0]+20)=max(0,0+20)=20 dp[4]=max(dp[4],dp[0]+value[2]=max(35,30)=35
问题:
(1)遍历顺序问题:
这里与二维数组的遍历顺序不同,先正序遍历了物品的重量,再逆序遍历了背包的容量,原因是:逆序时你会发现在第一遍遍历时:我的dp[4]不会影响到dp[3],直到第一遍遍历快结束时,dp[1]才给了我们一个具体的数值,到第二遍遍历时,我们才是真正意义上的递推
由于比较难理解,我比较sha地把递推的过程都写了出来(在上面)
接下来理解下被”影响“的含义:假设我的第二层是正序遍历
dp[1]=max(dp[1],dp[0]+15) j=1 ->> dp[1]=max(0,15)=15 dp[2]=max(dp[2],dp[1]+15) j=2 ->> dp[2]=max(0,30)=30 dp[3]=max(dp[3],dp[4-1]+15)=max(dp[3],dp[2]+15) dp[3]=45 dp[4]=max(dp[4]),dp[4-1]+15)=max(dp[4],dp[3]+15) dp[4]=60
你会发现:物品1被重复放进了背包,这与题目规定的:物品只有一件的条件相违背!
同时,两个for循环的先后顺序也不能颠倒
(2)初始化问题:
一开始是将数组全部初始化为0,经过一次遍历后,数组别重新修改为了15(value[0])
(3)递推公式问题:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);(很好理解:大过你就覆盖)
此时的dp[j]和上面的dp[i][j]中的dp[j]含义一样:当背包容量为j时,背包存储的最大价值
思路解析:
这道题实质是将数组分为两部分:
(1)对数组进行求和,总和记录为sum
·····1如果sum为奇数,那么就证明sum无法被拆为两部分,return false;
·····2如果sum为偶数,那么用target=sum/2
(2)相当于只要证明容量为target的背包只要当容量满的时候,它的价值为target即可完成
对于数组中的数字大小,即是它的重量又是它的价值
递推公式为:dp[j]=max(dp[j],dp[j-nums[i]]+nums[i])
(3)判断->相当于(2)的话用代码表示即为:dp[target]==target-->return true; else false;
class Solution { public: bool canPartition(vector<int>& nums) { int sum = 0; vector<int>dp(10001, 0); for (int i = 0; i < nums.size(); i++) sum += nums[i]; if (sum % 2) return false; int target = sum / 2; for (int i = 0; i < nums.size(); i++) { for (int j = target; j >= nums[i]; j--) { dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); } } if (dp[target] == target) return true; return false; } };
问:为什么dp数组开10001这么大?
答:因为
1 <= nums.length <= 200
1 <= nums[i] <= 100
思路解析:
这道题很容易联想到将数组分为大小最为接近的两堆,以此得到最优值
(1)对数组进行求和,总和记录为sum
将target=sum/2 分为两半,target和sum-target
但是注意:当sum为偶数时,这两堆相等
当sum为奇数时,由于target=sum/2,向下取整,所以target比sum-target小
(2)既然是求剩下石头重量,那么我们只需让这两个相减即可,当然是大的-小的
所以我们就确定了最后一步:return (sum-dp[target])-dp[target];
接下来的目标就是求dp[target]
(3)递推公式
dp[j]=max(dp[j],dp[j-nums[i]]+nums[i])
同样的在数组中,数字的大小,即是它的价值也是它的重量
class Solution { public: int lastStoneWeightII(vector<int>& nums) { int sum = 0; vector<int> dp(15001, 0); for (int i = 0; i < nums.size(); i++) sum += nums[i]; int target = sum / 2; for (int i = 0; i < nums.size(); i++) { for (int j = target; j >= nums[i]; j--) { dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); } } return sum - dp[target] - dp[target]; } };
解释:dp的大小应该不用我过多解释了^ ^
思路解析:
这道题与前两题不同的是:前两题求的是价值,这道题求是的数量
由于只有加减法:那么我们就假设加法的数量为x,总数为sum,那么减法的数量为sum-x
即有:x-(sum-x)=target,target为目标值
我们可以进行讨论:
如果总数sum<target:也就是说全部的数字为+或-,的绝对值都小于target了,return 0;
经过上式则可得x的表达式为:x=(target+sum)/2;
由于x必定为整数,所以target和sum的值必须为偶数
这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。
本题则是装满有几种方法。其实这就是一个组合问题了
递推公式为:dp[j] += dp[j - nums[i]]
dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
int findTargetSumWays(vector<int>& nums, int target) { int sum = 0; for (int i = 0; i < nums.size(); i++) sum += nums[i]; if (abs(target) > sum) return 0; if ((sum + target) % 2 == 1) return 0; int bagsize = (sum + target) / 2; vector<int> dp(bagsize + 1, 0); dp[0] = 1; for (int i = 0; i < nums.size(); i++) { for (int j = bagsize; j >= nums[i]; j--) { dp[j] += dp[j - nums[i]]; } } return dp[bagsize]; }
解释:关于dp的初始化:dp[0]=1,当容量为0时,填满它只有一种方法:不填
这么解释虽然很牵强,但是从递推公式我们可以看出,如果初始化为0,那么所有的结果均为0
思路解析:
题目的意思:让你返回满足这个条件的最长串
这道题有难…………,因为它的重量变为了两种,0的数量和1的数量,价值呢?字符串的个数
可以简单地理解为:重量是要消耗的,价值是我们想要的结果
(1)遍历整个大串,在大串中遍历小串,在小串中用字符去遍历,统计0和1的个数
(2)遍历顺序:最外层是大串(物品的重量),内层是(背包的容量)(有两个内层!)
(3)递推公式:因为这里是求最优(而不是3题的方法数),所以并非组合问题
dp[i][j]=max(dp[i][j],dp[i-zerosize][j-onesize]+1);
class Solution { public: int findMaxForm(vector<string>& strs, int m, int n) { vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0)); // 默认初始化0 for (string str : strs) { // 遍历物品 int oneNum = 0, zeroNum = 0; for (char c : str) { if (c == '0') zeroNum++; else oneNum++; } for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历! for (int j = n; j >= oneNum; j--) { dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); } } } return dp[m][n]; } };
(1)次数不同
与01背包不同的是,同一件物品,在01背包问题中,只能取1次,而在完全背包问题中,可取无数次(只要背包容量够)
(2)次数不同导致了遍历顺序不同
在01背包中,第二层的for循环我们逆序遍历背包的容量
在完全背包中,第二层的for循环,我们可以正序遍历背包的容量
(3)组合问题和排序问题
组合问题的遍历方式:先物品再背包(都是正序)
排列问题的遍历方式:变背包再物品(都是正序)
完全背包模板:
// 先遍历物品,在遍历背包 void test_CompletePack() { vector<int> weight = {1, 3, 4}; vector<int> value = {15, 20, 30}; int bagWeight = 4; vector<int> dp(bagWeight + 1, 0); for(int i = 0; i < weight.size(); i++) { // 遍历物品 for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } cout << dp[bagWeight] << endl; } int main() { test_CompletePack(); }
思路解析:
不考虑顺序,求组合总数
由于是组合总数:所以遍历顺序是:先物品再背包
由于是组合总数:所以递推公式是:dp[j]=dp[j-nums[i]]
class Solution { public: int change(int amount, vector<int>& coins) { vector<int> dp(amount + 1, 0); dp[0] = 1; for (int i = 0; i < coins.size(); i++) { // 遍历物品 for (int j = coins[i]; j <= amount; j++) { // 遍历背包 dp[j] += dp[j - coins[i]]; } } return dp[amount]; } };
思路解析:
这道题就是妥妥的排列总数题
由于是排列总数:所以遍历顺序为:先背包再物品
由于是排列总数:所以递推公式为:dp[i] += dp[i - nums[j]];(一摸一样)
class Solution { public: int combinationSum4(vector<int>& nums, int target) { vector<unsigned int> dp(target + 1, 0); dp[0] = 1; for (int i = 0; i <= target; i++) { // 遍历背包 for (int j = 0; j < nums.size(); j++) { // 遍历物品 if (i - nums[j] >= 0 ) { dp[i] += dp[i - nums[j]]; } } } return dp[target]; } };
解释:防止溢出我把dp的类型换为了unsigned int,期间我试过了int,long long都会溢出
索性换为了unsigned int,没想到居然过了^ ^
题目数据保证答案符合 32 位整数范围
等我问大佬清楚了,再来补充^ ^