01 背包(二维):
有 n 件物品和一个最多能装重量 w 的背包。第 i 件物品的重量是 weight[i],得到的价值是 value[i]。每件物品只能使用一次,求解将哪些物品装入背包里物品价值总和最大。向背包中装物品,有点类似在限制条件下的组合问题,可以使用暴力解法穷举所有可能的解,得出最优解。每种物品只有两种状态,装入背包 和 不装入背包,一共有 n 种物品,所以时间复杂度为 O(2^n)。
其实,背包问题是经典的动态规划问题,因为当前背包中的物品价值是根据此前背包中的物品和当前物品决定的,存在对此前状态的继承与叠加,动态规划刚好可以解决这种问题。下面,我们按照动态规划五部曲,分析背包问题。
1)确定 dp 数组的意义:
在背包问题中,我们选用二维的 dp 数组,因为一共有 物品种类 和 背包容量 两个维度。dp[i][j] 表示容量为 j 的背包在物品 0 ~ i 中能够容纳的最大价值;
2)确定递推公式:
针对每一件物品 i,只有 选取 和 不取 两种状态。如果 不选取 当前下标为 i 的物品,此时背包 dp[i][j] 中的价值和上一个状态中的价值 dp[i - 1][j] 相同,因为没有向背包中添加任何物品,价值没有变化。如果 选取 当前下标为 i 的物品,此时背包 dp[i][j] 中的价值为 此前背包中的物品总价值 + 当前物品的价值,即 dp[i][j] = dp[i][j - weight[i] ] + value[i]。细心的同学可能会发现,可以将两个递推公式整合,当 weight[i] 为 0 时,value[i] 为 0,背包的容量没有发生变化,没有向背包添加物品,value[i] 自然也为零。但是这里存在一个取舍的策略,最大化利用背包容量,当前物品不装入背包,比装入背包对于背包中物品价值影响收益更大。所以要取两个状态的 max;
3)初始化策略:
在物品维度维度 0 ~ i,当背包容量为 0 时,无法装入任何东西,此时对应的背包价值总为 0。在背包维度为 0 ~ j,因为每个物品只能使用一次,此时对应的背包价值总为物品 0 的价值。
4)遍历方向:
先遍历物品 or 先遍历背包 都可以,因为背包价值总是由此前背包的价值得到的,先计算哪个维度都一样,保证从二维数组的左上向右下即可;
5)当遇到错误的时候,打印 dp 数组 debug。
//二维dp数组实现
#include <bits/stdc++.h>
using namespace std;
int n, bagweight;// bagweight代表行李箱空间
void solve() {
vector<int> weight(n, 0); // 存储每件物品所占空间
vector<int> value(n, 0); // 存储每件物品价值
for(int i = 0; i < n; ++i) {
cin >> weight[i];
}
for(int j = 0; j < n; ++j) {
cin >> value[j];
}
// dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
// 初始化, 因为需要用到dp[i - 1]的值
// j < weight[0]已在上方被初始化为0
// j >= weight[0]的值就初始化为value[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++) { // 遍历行李箱容量
// 如果装不下这个物品,那么就继承dp[i - 1][j]的值
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
// 如果能装下,就将值更新为 不装这个物品的最大值 和 装这个物品的最大值 中的 最大值
// 装这个物品的最大值由容量为j - weight[i]的包任意放入序号为[0, i - 1]的最大值 + 该物品的价值构成
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() {
while(cin >> n >> bagweight) {
solve();
}
return 0;
}
01 背包(一维):
可以将二维的 dp 数组压缩至一维,回想我们对于 dp 数组的定义,在背包容量为 j 时,从物品 0 ~ i 中取物品,放置背包中,能得到的最大背包价值。注意是价值!!!从二维数组的列角度分析每一个列向量的含义,就会发现其实每一个列对应的是容量为 j 时,背包中最大价值的变化。如果我们将所有行压缩,为每一个列向量保留一个元素,即使用当前状态覆盖此前状态,隐式地存储此前状态。就像台历一样,当前为 10 月,那么此前的状态肯定是 1 ~ 9 月,因为 10 月是根据 1 ~ 9 月的信息推导而来,即使没有显式地说明 1 ~ 9 月,但是你也一定知道,此前经历过了这些月份。一种滚动的概念。
递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i];
初始化:全 0;
遍历顺序:从后向前,因为每一种物品只能使用一次,因此如果从前向后遍历,同一个物品会重复使用多次。
416.分割等和子集
想要把一个 正整数 非空 数组分割成两个子集,使得两个自己的元素和相等,即寻找一个子集,子集由一些元素组成,这些元素的和为整个数组元素和的一半。
每个数组中的元素都可以被看作是物品,只能使用一次,其值既是价值也是重量;子集的和为背包的容量,将该问题转换为01背包问题。
class Solution {
public:
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;
int dp[10001] = {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;
}
};