文章目录
- 01背包
- 二维dp数组
- 一维dp数组 滚动数组
- 416. 分割等和子集
- 1049.最后一块石头的重量II
- 494. 目标和
- 474. 一和零
01背包
完全背包的物品数量是无限的,01背包的物品数量只有一个。
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
背包最大重量为4,问背包能背的物品最大价值是多少?物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
二维dp数组
- 确定dp数组以及下标的含义:二维数组dp[i][j],表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少,如
背包重量j | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
物品0 | |||||
物品1 | |||||
物品2 |
- 确定递推公式,有两个方向推出来dp[i][j]:
- 不放物品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][j]=dp[i - 1][j - weight[i]] + value[i],物品i的价值为value[i]。意味着,物品i的重量小于背包j的重量时,背包放物品i得到的最大价值。
- 递归公式:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
- dp数组如何初始化
情况1:j=0时,dp[i][0]=0,此时背包容量j为0,无论选取什么物品,背包价值总和为0
情况2:i=0时,dp[0][j],表示存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
当 j < weight[0]时,dp[0][j]=0,因为背包容量比编号0的物品重量还小;
当j >= weight[0]时,dp[0][j]=value[0],因为背包容量放足够放编号0物品。
背包重量j | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
物品0 | 0 | 15 | 15 | 15 | 15 |
物品1 | 0 | ||||
物品2 | 0 |
-
确定遍历顺序
先遍历物品,或者先遍历背包都可以 -
C++实现
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]);
}
}
/*
// weight数组的大小 就是物品个数
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
for(int i = 1; i < weight.size(); i++) { // 遍历物品
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();
}
一维dp数组 滚动数组
在二维数组的基础上,把上一层结果覆盖在当前层,即dp[i - 1]那一层拷贝到dp[i]上
- 确定dp数组以及下标的含义:一维数组dp[j],容量为j的背包,所背的物品价值可以最大为dp[j]
背包重量j | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
物品0 | |||||
物品1 | |||||
物品2 |
- 确定递推公式,有两个方向推出来dp[j]:
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])
- 不放物品i:由dp[j]本身推出,相当于二维dp数组的dp[i - 1][j]。此时物品i的重量大于背包j的重量时,物品i无法放进背包中,背包内的价值不变。
- 放物品i:由dp[j - weight[i]] + value[i]推出,物品i的价值为value[i],dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。物品i的重量小于背包j的重量时,背包放物品i得到的最大价值。
- 递归公式:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
- dp数组如何初始化
情况1:j=0时,dp[0]=0,此时背包容量j为0,无论选取什么物品,背包价值总和为0
情况2:j≠0时,dp[j]会被覆盖更新。
背包重量j | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
物品0 | 0 | 15 | 15 | 15 | 15 |
物品1 | 0 | ||||
物品2 | 0 |
-
确定遍历顺序
二维dp遍历的时候,背包容量是从小到大,先遍历物品或者先遍历背包都可以,正序遍历
一维dp遍历的时候,背包是从大到小,只能先遍历物品再遍历背包容量,倒序遍历背包容量,都是是为了保证物品i只被放入一次 -
C++实现
void test_1_wei_bag_problem() {
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 = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_1_wei_bag_problem();
}
416. 分割等和子集
背包问题确定
元素只能使用一次,不可重复放入,01背包
背包的体积为sum / 2
背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
背包如果正好装满,说明找到了总和为 sum / 2 的子集。
步骤
-
确定dp数组以及下标的含义
题目的每一个元素的数值既是重量,也是价值。
那么dp[j]表示背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。
背包容量为target, dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了。 -
确定递推公式
物品i的重量是nums[i],其价值也是nums[i],那么递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
-
dp数组如何初始化
j=0时,dp[0]=0,j≠0时,dp[j]会被覆盖更新。
要注意的是,如果题目给的价值都是正整数,那么非0下标都初始化为0;如果给的价值有负数,那么非0下标就要初始化为负无穷。 -
确定遍历顺序
使用一维dp数组,只能先遍历物品,即物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历 -
举例推导dp数组
dp[j]的数值一定是小于等于j的,如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j。用例1,输入[1,5,11,5] 为例,target = (1+5+11+5) / 2 = 11
下标i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 1 | 1 | 1 | 5 | 6 | 6 | 6 | 6 | 10 | 11 |
- C++实现
class Solution {
public:
bool canPartition(vector<int>& nums) {
// 1 <= nums.length <= 200,1 <= nums[i] <= 100
//dp数组总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
vector<int> dp(10001, 0);//dp数组初始化
int sum = 0;
for(int i=0; i<nums.size(); i++)
{
sum += nums[i];
}
if(sum % 2 ==1) return false;
int target = sum / 2;
//01背包
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;
}
};
1049.最后一块石头的重量II
步骤
-
确定dp数组以及下标的含义
物品的重量为stones[i],物品的价值也为stones[i]。
dp[j]表示容量(重量)为j的背包,最多可以背最大重量为dp[j],最多可以装的价值为 dp[j] = 最多可以背的重量为dp[j] -
确定递推公式
物品i的重量和价值都是stones[i],那么递推公式:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
-
dp数组如何初始化
target=最大重量的一半,最大重量100*30=3000
j=0时,dp[0]=0,重量不可能是负数。 -
确定遍历顺序
使用一维dp数组,只能先遍历物品,即物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历 -
举例推导dp数组
dp[j]的数值一定是小于等于j的,如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j。输入[2,4,1,1],target = (2 + 4 + 1 + 1)/2 = 4
-
C++实现
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
vector<int> dp(1501, 0);
int sum = 0;
for(int i=0; i<stones.size(); i++) sum += stones[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];
}
};
494. 目标和
加法总和 + 减法总和 = sum,加法总和为x,那么 x - (sum - x) = target,left = (target + sum)/2 。
步骤
-
确定dp数组以及下标的含义
一维dp数组,dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
二维dp数组,dp[i][j]表示:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。 -
确定递推公式
nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。
求组合类问题的公式,都类似:dp[j] += dp[j - nums[i]];
-
dp数组如何初始化
初始化 dp[0] 为 1
情况剔除:如果target > sum,无解;如果(target+sum) % 2 =1,无解 -
确定遍历顺序
使用一维dp数组,只能先遍历物品,即物品遍历在外循环,遍历背包在内循环,且内循环倒序遍历,即nums在外循环,target在内循环,且内循环倒序。 -
举例推导dp数组
nums: [1, 1, 1, 1, 1], target: 3
bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4
-
C++实现
class Solution {
public:
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((target + sum) % 2 ==1) return 0;
int bagsize = (target + sum) / 2;
//dp数组 初始化
vector<int> dp(bagsize + 1, 0);
dp[0] = 1;
//dp数组更新 先物品后背包 背包倒序
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];
}
};
474. 一和零
strs 数组里的元素就是物品,每个物品都是一个;m 和 n相当于是一个背包,两个维度的背包。
步骤
-
确定dp数组以及下标的含义
二维dp数组,dp[i][j]:最多有i个0和j个1的strs的最大子集的大小 -
确定递推公式
- dp[i][j] 可以由strs里的前一个字符串推导,strs里的字符串有zeronum个0,onenum个1,那么dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。
- 遍历的过程中,取dp[i][j]的最大值。
- 递推公式:
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
- 01背包的递推公式:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
,字符串的zeronum和onenum相当于物品的重量weight[i],字符串本身的个数相当于物品的价值value[i]
-
dp数组如何初始化
初始化 dp[0] 为0
物品价值不会是负数,dp数组初始为0,保证递推的时候dp[i][j]不会被初始值覆盖 -
确定遍历顺序
使用一维dp数组,只能先遍历物品,即物品遍历在外循环,遍历背包在内循环,且内循环倒序遍历,即strs里的字符串在外循环,m和n在内循环,且内循环倒序。 -
举例推导dp数组
以输入:[“10”,“0001”,“111001”,“1”,“0”],m = 3,n = 3为例,
-
C++实现
注意字符串的遍历
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
//dp数组 默认初始化为0
vector<vector<int>> dp(m+1, vector<int>(n+1, 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];
}
};