代码随想录刷题攻略---动态规划2---01背包+多重背包、完全背包概念

文章讲述了科学家小明面临行李打包问题,需在有限空间内选择最有价值的研究材料。利用动态规划的动规五部曲,通过构建dp数组和递推式解决背包问题,以求得最大价值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

01背包概念

01背包问题:

有 n 件物品和一个最多能背重量为 w 的背包。第i件物品的重量是 weight[i],得到的价值是 value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解了。

这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢?

每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是 O(2^n),这里的 n 表示物品数量。

所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!


01背包的动规五部曲

1、定义

dp[i][j] 表示从下标为 [0-i] 的物品里任意取,放进容量为 j 的背包,价值总和最大是多少

  • 不放物品 i:背包容量为j,里面不放物品i的最大价值是 dp[i - 1][j] 。

  • 放物品 i:背包空出物品i的容量后,背包容量为 j - weight[i],dp[i - 1][j - weight[i]] 为背包容量为 j - weight[i] 且不放物品i的最大价值,那么 dp[i - 1][j - weight[i]] + value[i](物品i的价值),就是背包放物品 i 得到的最大价值。

2、递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

3、初始化:初始 dp[i][j] 中,i = 0 和 j = 0 的 2 种情况。

4、遍历方向:从递推公式可知, dp[i][j] 是从左上角遍历而来的。

01背包问题之滚动数组

由于 dp[i][j] 是由正上方和左上角推出的,我们可以把上一层 拷贝 到当前层,再在当前层直接进行计算覆盖原来的数。这样就可以把二维数组降维成一维数组了。

1、含义:在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。

2、递推公式:一维dp数组,其实就是上一层 dp[i-1] 这一层 拷贝的 dp[i]来。

所以在 上面递推公式的基础上,去掉i这个维度就好。

递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

可以看出相对于二维dp数组的写法,就是把dp[i][j]中i的维度去掉了

3、初始化:

dp[0]=0,dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。

这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了

4、遍历顺序:

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

那么问题又来了,为什么二维dp数组遍历的时候不用倒序呢?

因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!

看着弹幕总结了一下,就是一维数组的结果是复制上一层进行计算得到的,如果是正序遍历,就是修改左上方数据的值,导致本层的计算结果不正确;倒序遍历可以保证左边的值没有被修改,先计算本层最右边的结果,再往左更新,才能得到正确的结果。

01背包、完全背包、多重背包的概念

如下,主要是每个物品数量的不同。

 而完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。

例题1

小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。 

小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。

动规五部曲

1.确定dp数组以及下标的含义

dp[i][j] 的含义:在下标 [0~i] 的物品中任取,装进容量为 j 的背包,所获得的最大价值为 dp[i][j].

官方给的图:

2.确定dp数组的递推式

由1,当前dp[i][j]的状态是由上一个 dp[i-1][j] 推导来的,dp[i-1][j] 已经做好了决策,那么到dp[i][j]的时候,我们可以选择放当前下标为 i 的物品,也可以选择不放。

如放,dp[i][j]=dp[i][j-weight[j]]+value[i]

如不放,dp[i][j]=dp[i-1][j]

此时我们要求dp最大值,故 dp[i][j]=max(dp[i-1][j],dp[i][j-weight[j]]+value[i])

3.初始化dp数组

对于dp二维数组,我们通常将i=0,j=0的一行一列进行初始化

当i=0时,dp[0][j]表示从下标为0的物体中取物装进背包。这时若  j>= weight[0] ,则放入背包,反之dp [0][j]都为0

当j=0时,背包容量为0,此时dp[i][0]都为0,放不下任何物品。

4.确定遍历顺序

由递推式可知,有两个遍历的维度:物品与背包重量,先遍历哪个都行,因为dp[i][j]需要靠dp[i-1][j]推导,那么在这里先遍历物品i。

5.举例推导dp数组

做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!

code

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();
}

 例题2:分割等和子集

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

注意:每个数组中的元素不会超过 100 数组的大小不会超过 200

动规五部曲

1.确定dp数组以及下标的含义

如题,若集合元素的和为sum,分割为两个子集,那么如果其中一个子集的集合元素和为sum/2,另一个子集元素的和自然而然也为sum/2,所以我们只需验证,集合中能否找出集合元素元素和为sum/2的序列,也就是一个容量为sum/2的背包是否能被填满 的0-1背包问题

dp[j]的含义:容量为j的背包,所能装的物品的最大价值为dp[j]。这样,当dp[j] == sum/2时,就证明true。

2.确定dp数组的递推式

根据dp[j]的含义,dp[j]每次在 放当前物品不放当前物品 中取最大值,而物品的重量和价值是同一个表达式,即nums[i]。故dp[j] = max(dp[j], dp[j - weight[i]] + values[i])

3.初始化dp数组

dp[0]=0,当 j=0 时,背包容量为 0,放不下任何物品。

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

4.确定遍历顺序

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。因为如果正序遍历背包的话,背包从小到大的过程中可能会放入多次的i相同的物品,导致一个背包放入了多次的相同物品。

那么可以先遍历背包再遍历物品吗?不可以。如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值因此需要保证左边的值仍然是上一层的,从右向左覆盖

5.举例推导dp数组

code 

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 != 0)
            return false;
        //dp[j]:容量为j的背包能装下的最大值
        vector<int> dp(10001,0);//顺便初始化了  取10001是因为所有元素的和上限为200*100,我们区一半,上限为10000
        
        //递推公式
        for(int i = 0; i < nums.size(); i++){
            for(int j = sum/2; j >= nums[i]; j--){
                dp[j] = max(dp[j], dp[j-nums[i]] + nums[i]);
            }
        }
        if(dp[sum/2] == sum/2) return true;
        return false;
    }
};

例题3:最后一块石头的重量II

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

动规五部曲

1.确定dp数组以及下标的含义

dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]

此题尽可能将容量为 sum/2 的背包装满,这是第一堆石头,再用 sum- sum/2,这是第二堆石头。

2.确定dp数组的递推式

dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]); 物品的价值和体积是同一个数组

3.初始化dp数组

全为0即可,不影响  max 的作用。

4.确定遍历顺序

为了不干扰到右边的数组计算,我们不从左边开始计算,保留上一层的数据,所以我们先从右边开始进行处理。

5.举例推导dp数组

code

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for(int i = 0; i < stones.size(); i++){
            sum += stones[i];
        }
        //sort(stones.begin(), stones.end()); 排序了应该也不行,每次碰撞都需要重新排序
        //此题像是尽可能将容量为 sum/2 的背包装满, 再用总和 - sum/2
        vector<int> dp(1501,0);//dp[j]表示容量为 j 的背包尽可能可以装多少石头
        for(int i = 0; i < stones.size(); i++){
            for(int j = sum/2; j >= stones[i]; j--){
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - dp[sum/2] - dp[sum/2];//一堆石头的总重量是dp[sum/2],另一堆就是sum - dp[sum/2]。
    }
};

例题4:目标和

给你一个非负整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

动规五部曲

既然为target,那么就一定有 left组合 - right组合 = target。

left + right = sum,而sum是固定的。right = sum - left

left - (sum - left) = target 推导出 left = (target + sum)/2

target是固定的,sum是固定的,left就可以求出来。

此时问题就是在集合nums中找出和为left的组合

1.确定dp数组以及下标的含义&确定dp数组的递推式

dp[j] 表示将容量为 j 的背包装满一共有 dp[j] 种方法,举个例子,dp[5] 表示把容量为 5 的背包填满的方法。

如果还有一个 4,那么 dp[5] = dp[1];

如果还有一个 3,那么 dp[5] = dp[2];

如果还有一个 2,那么 dp[5] = dp[3];

如果还有一个 1,那么 dp[5] = dp[4];

.........

所以 dp[j] += dp[ j - numbers[i] ]

2.初始化dp数组

dp[0]=1

3.确定遍历顺序

老规矩,先正序遍历物品,在逆序遍历背包容量。

4.举例推导dp数组

code

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(int i = 0; i < nums.size(); i++)
            sum += nums[i];
        if(abs(target) > sum) return 0;
        if((sum + target) % 2 != 0) return 0;
        vector<int> dp((sum + target)/2+1,0);
        dp[0] = 1;//全都初始化为0时,别忘了dp[0] = 1
        //开始递推公式
        for(int i = 0; i < nums.size(); i++){
            for(int j = (sum + target)/2; j >= nums[i]; j--){
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[(sum + target)/2];
    }
};

例题5:一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

 

动规五部曲

1.确定dp数组以及下标的含义

dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]

2.确定dp数组的递推式

dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。

dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。

然后我们在遍历的过程中,取dp[i][j]的最大值。

所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

此时大家可以回想一下01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])

3.初始化dp数组

因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。

4.确定遍历顺序

本题,物品就是strs里的字符串,背包容量就是题目描述中的m和n。

for (string str : strs) { // 遍历物品
    int oneNum = 0, zeroNum = 0;
    for (char c : str) {
        if (c == '0') zeroNum++;
        else oneNum++;
    }
    for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
        for (int j = n; j >= oneNum; j--) {
            dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
        }
    }
}

5.举例推导dp数组

 code

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
         vector<vector<int>> dp(m+1, vector<int> (n+1,0));
         for(string str : strs){
            int oneNum = 0, zeroNum = 0;
            for (char c : str) {
                if (c == '0') zeroNum++;
                else oneNum++;
            }
            for(int i = m; i>= zeroNum; i--){
                for(int j = n; j >= oneNum; j--)
                    dp[i][j] = max(dp[i][j], dp[i-zeroNum][j - oneNum] + 1);
            }
        }
        return dp[m][n];
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值