今日任务
- 01背包问题,你该了解这些!
- 01背包问题,你该了解这些! 滚动数组
- 416. 分割等和子集
01背包问题 二维数组
理论基础:代码随想录
题目链接:题目页面
01 背包
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
暴力解法:
每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况
暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化二维dp数组
1 确定dp数组及含义:
dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少
2 递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
3 dp数组初始化:所有dp[i][0]均为0;当 j < weight[0]的时候 dp[0][j] 是 0,当j >= weight[0]时,dp[0][j] 是value[0]
4 确定遍历顺序:有两个遍历的维度,物品与背包重量,先遍历物品更好理解
5 推导dp数组:如下,无参数版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(); }
//二维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数组的基础上,其实可以把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数组:
1 dp[j]:容量为j的背包,所背的物品价值可以最大为dp[j]
2 递推公式:dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i;一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
- 3 初始化:都初始为0就可以了
- 4 遍历顺序:一维dp遍历的时候,背包是从大到小,倒序遍历是为了保证物品i只被放入一次,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了;两个嵌套for循环的顺序,先遍历物品嵌套遍历背包容量,不可颠倒
- 5 推导dp数组:无参数版
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(); }
// 一维dp数组实现
#include <iostream>
#include <vector>
using namespace std;int main() {// 读取 M 和 Nint M, N;cin >> M >> N;vector<int> costs(M);vector<int> values(M);for (int i = 0; i < M; i++) {cin >> costs[i];}for (int j = 0; j < M; j++) {cin >> values[j];}// 创建一个动态规划数组dp,初始值为0vector<int> dp(N + 1, 0);// 外层循环遍历每个类型的研究材料for (int i = 0; i < M; ++i) {// 内层循环从 N 空间逐渐减少到当前研究材料所占空间for (int j = N; j >= costs[i]; --j) {// 考虑当前研究材料选择和不选择的情况,选择最大值dp[j] = max(dp[j], dp[j - costs[i]] + values[i]);}}// 输出dp[N],即在给定 N 行李空间可以携带的研究材料最大价值cout << dp[N] << endl;return 0;
}
416.分割等和子集 - Medium
题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
思路: 转换成背包容易为 1/2 数组和的 01背包问题。时间复杂度:O(n^2),空间复杂度:O(n),虽然dp数组大小为一个常数,但是大常数
class Solution {
public:bool canPartition(vector<int>& nums) {int sum = 0;// dp[i]中的i表示背包内总和// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了vector<int> dp(10001, 0);for (int i = 0; i < nums.size(); i++) {sum += nums[i];}// 也可以使用库函数一步求和// int sum = accumulate(nums.begin(), nums.end(), 0);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]);}}// 集合中的元素正好可以凑成总和targetif (dp[target] == target) return true;return false;}
};
今日总结
01背包是后续背包问题的基础