代码随想录算法训练营第三十一天|动态规划:01背包理论基础、01背包理论基础(滚动数组)

动态规划:01背包理论基础

1. dp[i][j]: 表示0到i个物品放入容量为j的背包中,价值总和最大是多少

2. dp[i][j]的状态取决于,第i个物品要不要放入这个背包。

不放物品i:dp[i-1][j] (其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)

放物品i: (等于不放物品i时背包的状态加上i的价值)dp[i-1][j-weight[i]] + value[i]

取两者的最大值。

3. dp[i][j]的初始化,由上两个公式可以看出,假如dp是一个表格,dp[i][j]是由它的正上方和左上角的数字推理出来的。所以我们需要初始化第一行和第一列的值。第一列都是0(背包没有容量),第一行容量大于物品1的格子都是value【0】(背包只放物品0)。

4. 确定遍历顺序

先遍历 物品还是先遍历背包重量呢?都可以,但先遍历物品更好理解。

5. 举例推导dp数组

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, V;
    cin>>n>>V;
    vector<int> weight(n+5);
    vector<int> value(n+5);
    for(int i=0; i<n; i++){
        cin>>weight[i];
    }
    for(int i=0; i<n; i++){
        cin>>value[i];
    }
    vector<vector<int>> dp(n, vector<int>(V+1, 0));//bagweight V需要加一
    for(int i = weight[0]; i <= V; i++){
        dp[0][i] = value[0];
    }
    for(int i=1; i<n; i++){
        for(int j=0; j<=V; j++){//从0到V一共V+1个不同重量
            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[n-1][V]<<endl;
    return 0;
}

01背包理论基础(滚动数组)

其实可以发现如果把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](一维数组,也可以理解是一个滚动数组)。

1. dp[j] 容量为j的背包能放下多少价值的东西。

2. 一维dp数组的递推公式:此时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. 一维dp数组如何初始化:初始为0

4. 一维dp数组遍历顺序:

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]);

    }
}

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

倒序遍历是为了保证物品i只被放入一次!

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数组的写法,比较直观简洁,而且空间复杂度还降了一个数量级!

在后面背包问题的讲解中,我都直接使用一维dp数组来进行推导

416. 分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

只有确定了如下四点,才能把01背包问题套到本题上来。

  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入

1. dp[j]的含义:dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]

2. 递推公式:01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

3. 初始化:dp[0]一定是0。如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。

4. 确定遍历顺序:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

5. 举例推导dp数组

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum=0;
        vector<int> dp(10005, 0);
        for(int i=0; i<nums.size(); i++){
            sum+=nums[i];
        }
        if(sum%2==1) return false;
        int target = sum/2;
        for(int i=0; i<nums.size(); i++){
            for(int j=target; j>=nums[i]; j--){
                dp[j] = max(dp[j-nums[i]]+nums[i], dp[j]);
            }
        }
        if(dp[target]==target) return true;
        return false;
    }
};
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n),虽然dp数组大小为一个常数,但是大常数

题目中物品是nums[i],重量是nums[i],价值也是nums[i],背包体积是sum/2。

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值