0-1背包基础理论
01 背包
问题描述
有 n 件物品和一个最多能背重量为 w 的背包。第 i 件物品的重量是 weight[i],得到的价值是 value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
暴力解法
- 每一件物品其实只有两个状态,取或者不取,因此可以用回溯法,求出所有的方案取最大值。时间复杂度
O(2^n)
动态规划
二维dp数组01背包
dp[i][j] 的含义:
- dp[i][j] 表示从下标为[0, i]的物品中任取,放进容量为 j 的背包,价值总和的最大值。
递推公式:
- 不放物品 i :dp[i - 1][j],即从[0, i - 1]的物品中任取,放进容量为 j 的背包的最大价值
- 放物品 i :dp[i - 1][j - weight[i]] + value[i],即从[0, i - 1]的物品中任取放进背包容量为 j - weight[i]的最大价值 + 物品 i 的价值
- 递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
dp数组初始化:
- dp[i][j] 由左上方向的值和上方的值共同推导得来
- 因此,需要初始化第一行第一列
- 第一行为把物品 0 分别放进重量为 0 到 j 的背包的最大价值
- 第一列为把物品[0, i] 放进重量为 0 的背包的最大价值
遍历顺序:
- 先遍历物品后遍历背包,或者先遍历背包后遍历物品都可以(因为从左上和上推导)
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
void test2_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];
}
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() {
test2_wei_bag_problem1();
return 0;
}
一维dp数组(滚动数组)
滚动数组:上一层可以重复利用,直接拷贝到当前层
- dp[j] 表示:容量为j的背包,所背的物品价值可以最大为dp[j]
- 递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
- 初始化:dp 数组初始化为 0
- 遍历顺序:外层物品,内层重量,内层倒序遍历(正序遍历会覆盖滚动数组的上一层的值)
- 上一层是不放物品i的值,下一层是有可能放了物品i的值,所以正序遍历会覆盖原有的值,导致物品i被加入多次
- 不可以颠倒内外层:背包容量一定是要倒序遍历,如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品
void test1_wei_bag_problem1() {
vector<int> weight = { 1, 3, 4 };
vector<int> value = { 15,20,30 };
int bagweight = 4;
vector<int> dp(bagweight + 1);
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() {
test1_wei_bag_problem1();
return 0;
}
416.分割等和子集
思路
动态规划
- 核心:从数组中任取数字使其和成为原数组和的一半
- 可转化为01背包问题,weight[i]==value[i]==nums[i]
- dp[j] 装满容量为j的背包的最大价值
- 当dp[j] == target时,说明背包被装满
- 递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
- 初始化为0
- 遍历顺序:内层从大到小,外层物品,内层背包
回溯
- 收集所有子集
- 如果有一个子集的和(子集大小<nums.size())==数组和的一半,则返回true
- 力扣超时
代码
动态规划
class Solution {
public:
/* 核心:从数组中任取数字使其和成为原数组和的一半
* 可转化为01背包问题,weight[i]==value[i]==nums[i]
* dp[j] 装满容量为j的背包的最大价值
* 当dp[j] == target时,说明背包被装满
* 递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
* 初始化为0
* 遍历顺序:内层从大到小,外层物品,内层背包
*/
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int i = 0; i < nums.size(); i++)
sum +=nums[i];
if (sum % 2 == 1) return false;
int target = sum / 2;
vector<int> dp(target + 1, 0);
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;
}
};
- 时间复杂度
O(n*target)
- 空间复杂度
O(target)
回溯
class Solution {
public:
bool flag = false;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex, int sum) {
if (!path.empty() && path.size() < nums.size()) {
int ans = 0;
for (int i = 0; i < path.size(); i++)
ans += path[i];
if (ans == sum) flag = true;
}
for (int i = startIndex; i < nums.size() && !flag; i++) {
path.push_back(nums[i]);
backtracking(nums, i+1, sum);
path.pop_back();
}
}
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int i = 0; i < nums.size(); i++)
sum +=nums[i];
if (sum % 2 == 1) return false;
backtracking(nums, 0 , sum/2);
return flag;
}
};
- 时间复杂度
O(2^n)
- 空间复杂度
O(n)