一、背包问题概述
一个背包有它的最大容量,然后有一些物品,物品有它的价值和体积,这是前提。背包问题就是寻找不同的方案来填满背包,从而使得背包内的物品价值最大。
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。这就是典型的01背包问题。
完全背包和01背包的区别就是,在完全背包中,所有的物品都可以无限次选取,默认物品的数目是无数个。而在01背包中,每个物品只能选取一次。
二、01背包问题思路
2.1 二维数组的01背包
dp数组的含义是:dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
然后确定递推公式,可以有两个方向推出来dp[i][j]:
- 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。
- 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值。
所以递归公式为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
关于初始化。
首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。
当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
在01背包的二维数组中,先遍历背包容量或者先遍历物品都是可以的。
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]);
}
}
cout << dp[weight.size() - 1][bagweight] << endl;
}
2.2 一维数组的01背包
在背包问题中,使用二维数组和一维数组时间复杂度没有区别,但是一维数组(滚动数组)的空间复杂度小了一个级别。下面介绍一维数组的01背包写法。
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。
这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
递推公式是dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);,那么初始化的时候,dp[0]初始化为0,因为容量是0,价值肯定是0. 其他位置也直接都初始化为0就可以了。因为后面都是取的最大值,那么保证能被新的值覆盖就可以了,就都初始化为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]);
}
}
注意,这里的遍历顺序中,在遍历背包容量j时,遍历顺序是从后往前的。和二维01背包的遍历顺序相反。这是因为,倒序遍历是为了保证物品i只被放入一次! 。但如果一旦正序遍历了,那么物品就会被重复加入多次!
为什么正序遍历物品就会被加入多次,而倒序遍历就不会呢?
是因为:倒序遍历的本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。 只有这样,才能保证本层的数值都是上一层的。如果是正序遍历,我们看到递推公式dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]),就没办法保证max括号里面的这个dp[i][j - weight[i]]到底是上一层的值还是本层的了。
这就是必须倒序遍历的原因!!!
滚动数组遍历01背包的时候,必须先遍历物品种类,再遍历背包容量!!!
01背包滚动数组写法测试代码:
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();
}
01背包典型题目
1、分割等和子集
题目链接:416. 分割等和子集 - 力扣(LeetCode)
**题目要求:**给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for(auto i:nums)
sum += i;
if(sum%2)
return false;
int val = sum/2;
vector<int>dp(val + 1,0);
for(int i = 0;i < nums.size();i ++)
{
for(int j = val;j >= nums[i];j --)
{
dp[j] = max(dp[j],dp[j - nums[i]] + nums[i]);//就是尽可能装的更多,看看最后能不能装满
}
}
return dp[val] == val;//看看能不能装满
}
};
2、目标和
题目要求: 给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。 返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
解题思路:
这是用背包思想解决组合问题,dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
其实也可以使用二维dp数组来求解本题,dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。
确定递推公式:当前的物品可以选择要或者不要,要的话,有dp[j - nums[i]]种组合可以拼凑出来,不要的话,那就是dp[j]种方式(这里的dp[j]是上一层的),所有递推公式是dp[j] += dp[j - nums[i]];
c++代码:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
//组合问题
int sum = 0;
for(auto i:nums)
sum += i;
if(target > sum || (sum - target)%2)
return 0;
int val = (sum - target)/2;
vector<int>dp(val + 1,0);
dp[0] = 1;//这里要初始化为1
for(int i = 0;i < nums.size();i ++)
{
for(int j = val;j >= nums[i];j --)
{
dp[j] += dp[j - nums[i]];
}
}
return dp[val];
}
};
3、一和零
题目链接: 474. 一和零 - 力扣(LeetCode)
题目要求: 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
解析:
这是一个二维01背包问题,即物品的“价值”是二维的,之前遇到的题目物品都只有一种价值。
其他部分的思路和普通01背包问题是一样的。
C++代码如下:
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>>dp(m + 1,vector<int>(n + 1));
//dp[i][j]的含义就是子集最多含有i个0和j个1的最大子集的长度
for(int i = 0;i < strs.size();i ++)
{
int count_0 = 0,count_1 = 0;
for(auto i : strs[i])
{
if(i == '0')
count_0 ++;
else
count_1 ++;
}
for(int j = m;j >= count_0;j --)
{
for(int k = n;k >= count_1;k --)
{
dp[j][k] = max(dp[j][k],dp[j - count_0][k - count_1] + 1);
}
}
}
return dp[m][n];
}
};