动态规划之背包问题——背包三讲(01背包,完全背包,多重背包)(一)

简介: 动态规划之背包问题——背包三讲(01背包,完全背包,多重背包)(一)

背包三讲


复现代码随想录DP专题


代码随想录 (programmercarl.com)

动态规划五部曲


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

确定递推公式

dp数组如何初始化

确定遍历顺序

打印数组(与自己的推导比较,看哪里错了)


01背包

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


1. 例题(二维数组解法)

题目

背包最大重量为4


重量

价值

物品0

1

15

物品1

3

20

物品2

4

30

问:背包能背的物品最大价值是多少?


思路

五部曲


数组含义


使用二维数组 dp[i][j],含义:从下标为 [ 0 ∼ i ] 的物品中任意取,放入容量为 j jj 的背包,价值总和最大为多少

递推公式由两个方向推出


一、不放物品 i的最大价值

背包容量为 j,但不放物品 i  时的最大价值,即 dp[i-1][j]

二、放物品 i的最大价值

先找到 dp[i-1][j-weight[i]],含义:i − 1 保证了不放物品 i,背包容量为 j − w e i g h t [ i ]其实就是为了给后续放物品 i  一个预留量,保证放了物品 i ii 后背包不会溢出,所以最大价值为 dp[i-1][j-weight[i]]+value[i]

递推公式:

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] )


数组初始化


背包容量 j 为零时,显然价值为零

只选物品0,即第一行,显然只要物品0重量小于等于背包重量 j,价值就为物品0的价值,否则为零

确定遍历顺序


先遍历背包还是物品都可以

dp[i][j] 所需的数据在其左上方

测试用例



代码展示

#include <iostream>
#include <cmath>
#include <vector>
using namespace std;
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() + 1, vector<int>(bagWeight + 1));
    for (int i = 1; i < bagWeight + 1; i++)
    {
        if (weight[0] <= i)
        {
            dp[0][i] = value[0];
        }
    }
    for (int j = 1; j < bagWeight + 1; j++)
    {
        for (int i = 1; i < weight.size(); i++)
        {
            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];
}
int main()
{
    test_2_wei_bag_problem1();
    return 0;
}


2. 例题(滚动数组解法)

还是之前的例子,可以用滚动数组将数组降为一维

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] )

由递推公式得: dp[i][j] 只和它的上一行有关,且有关元素的行数为 i − 1,列数 ≤ j 。那么我们就可以将数组压缩成一维

dp[i1][jweight[i]]

dp[i1][j]


欲求元素

看这张表,如果数组是一维的情况,那么在算出欲求元素后,其实是将欲求元素覆盖掉 dp[i-1][j],并且我们的遍历顺序也要改变。在二维情况下,我们是按从左到右的顺序求的,但在一维中,必须从右向左!因为在求欲求元素时,我们要保证 dp[i-1][j-weight[i]] 未被覆盖!同时,必须按照先遍历物品,再嵌套遍历背包的顺序。


01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次


举例:


假设物品0重量 w e i g h t [ 0 ] = 1 ,价值 v a l u e [ 0 ] = 15


如果是正序遍历


d p [ 1 ] = d p [ 1 − w e i g h t [ 0 ] ] + v a l u e [ 0 ] = 15


d p [ 2 ] = d p [ 2 − w e i g h t [ 0 ] ] + v a l u e [ 0 ] = 30


在第一行运行后 d p [ 1 ] 的状态已经发生改变,意味着已经放了物品0,而第二行运行后,叠加了改变后的 d p [ 1 ] ,意味着物品0被放了两次。


那么为什么之前在写二维数组的时候是正序的?


因为我们实际求的是上一行的状态,也就是不放当前物品的情况,只不过在滚动数组中,压缩了状态。也即当前元素的公式被运行后,当前元素的状态(在当前行,包括物品0)覆盖了先前状态(在上一行,不含物品0),所以一维中倒序是为了保证物品只被放入一次!

#include <iostream>
#include <cmath>
#include <vector>
using namespace std;
void test_1_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];
}
int main()
{
    test_1_wei_bag_problem1();
    return 0;
}


3. 分割等和子集(LeetCode-416)

题目

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


示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。


示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。


提示:


1 <= nums.length <= 200

1 <= nums[i] <= 100


思路

完全背包和01背包区别?


完全背包中一个商品可以放 无数次

01背包中一个商品只能放一次


本题如何套?


分割成两个子集且元素和相等,即背包容量为 s u m / 2 sum/2sum/2

物品重量为 n u m s [ i ] nums[i]nums[i],价值也为 n u m s [ i ] nums[i]nums[i]

背包正好装满,就说明找到了 s u m / 2 sum/2sum/2


五部曲


dp[i] 含义


背包容量为 i 时,最大可以凑出的子集和

递推公式


本题中,物品重量为 n u m s [ i ] ,价值也为 n u m s [ i ]

d p [ j ] = m a x ( d p [ j ] , d p [ j − n u m s [ i ] ] + n u m s [ i ] )


数组初始化


都初始化为零

遍历顺序


先遍历物品,嵌套遍历背包,且背包遍历要倒序,不懂的回看例题

测试用例



代码展示

class Solution
{
public:
    bool canPartition(vector<int> &nums)
    {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum % 2)
        {
            return false;
        }
        int target = sum / 2;
        vector<int> dp(target + 1);
        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;
    }
};


对比卡尔哥的解法,用 target+1 做为数组大小,优化了空间。遍历中遇到一个吻合的直接返回 true,优化了时间。


4. 最后一块石头的重量Ⅱ(LeetCode-1049)

题目

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


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


如果 x == y,那么两块石头都会被完全粉碎;

如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

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


示例 1:

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。


示例 2:

输入:stones = [31,26,33,21,40]
输出:5


示例 3:

输入:stones = [1,2]
输出:1


提示:


1 <= stones.length <= 30

1 <= stones[i] <= 100


思路

如何套?


目的是求 最小的可能重量,其实也就是凑重量尽可能相等的两堆。举个例子 10,1,2,2,2,2,2 。我们就可以分为重量为 11 1111 和 10 1010 的两堆。


五部曲


dp[j] 含义


重量为 j jj 的背包最大可以装的石头重量和

递推公式


本题中,物品重量为 s t o n e s [ i ] ,价值也为 s t o n e s [ i ]

d p [ j ] = m a x ( d p [ j ] , d p [ j − s t o n e s [ i ] ] + s t o n e s [ i ] )


数组初始化


设默认值零即可

遍历顺序


先遍历物品,嵌套遍历背包,且背包遍历要倒序

测试用例



代码展示

class Solution
{
public:
    int lastStoneWeightII(vector<int> &stones)
    {
        int sum = accumulate(stones.begin(), stones.end(), 0);
        int target = sum / 2;
        vector<int> dp(target + 1);
        for (int i = 0; i < stones.size(); i++)
        {
            for (int j = target; j >= stones[i]; j--)
            {
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - dp[target] - dp[target];
    }
};


开始时没有考虑到问题是最小的可能重量。在 stones = [31,26,33,21,40] 这个样例中:一堆为 31,26,21 重量和为 78 7878,另一堆为 33,40 重量和为 73 7373。按照代码求法,target=75。求的是 dp[75]。为什么不是求 dp[73]?回看数组定义:重量为 j jj 的背包最大可以装的石头重量和。可以看出:其实 dp[75] 与 dp[73] 是相等的


5. 目标和(LeetCode-494)

题目

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


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


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

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


示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3


示例 2:

输入:nums = [1], target = 1
输出:1


提示:


1 <= nums.length <= 20

0 <= nums[i] <= 1000

0 <= sum(nums[i]) <= 1000

-1000 <= target <= 1000


思路

怎么套?


分成两堆, l e f t − r i g h t = t a r g e t


又因为 l e f t + r i g h t = s u m


所以 l e f t = ( s u m + t a r g e t )/ 2


这和以前遇到的题目 不一样


先前:容量为 i 的背包,最多能装多少?


这次:填满容量为 i 的背包,有多少种方法?


五部曲


数组定义


dp[j] 表示:填满容量为 j 的背包,有 dp[j] 种方法

递推公式


分两种情况,一种是不包含当前物品的方法数量,另一种是包含当前物品的方法数量。我们要求的就是二者 之和。为了方便,直接使用一维数组展示:

d p [ j ] = d p [ j ] + d p [ j − n u m s [ i ] ]

数组初始化


如果是二维数组,也就是要初始化第一行,即只有物品零的情况,可以想见,填满背包容量为零的方法有一种,就是不装东西。但填满不为零的背包的方法却为零,除非物品重量等于背包容量。所以在一维数组中初始化应该是 dp[0]=1 其他为0

遍历顺序


先遍历物品,嵌套遍历背包,且背包遍历要倒序

测试用例



代码展示

class Solution
{
public:
    int findTargetSumWays(vector<int> &nums, int target)
    {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if ((sum < target) || ((sum + target) % 2) || ((sum + target) < 0))
        {
            return 0;
        }
        int left = (sum + target) / 2;
        vector<int> dp(left + 1);
        dp[0] = 1;
        for (int i = 0; i < nums.size(); i++)
        {
            for (int j = left; j >= nums[i]; j--)
            {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[left];
    }
};
目录
相关文章
动态规划之01背包问题和完全背包问题
动态规划之01背包问题和完全背包问题
|
算法 C语言 C++
【动态规划】背包问题(01背包,完全背包)
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
124 0
动态规划——01背包问题、完全背包问题
动态规划——01背包问题、完全背包问题
92 0
动态规划:01背包问题
动态规划:01背包问题
93 0
|
算法 决策智能
背包问题——01背包|完全背包 1
背包问题——01背包|完全背包
313 0
背包问题——01背包|完全背包 2
背包问题——01背包|完全背包
188 0
|
算法 JavaScript 前端开发
动态规划-01背包
前言 动态规划和递归是一对CP,递归通过将大问题分解成小问题以求得答案,而动态规划则是通过求解小问题来组成大问题的解。虽然其本质都是先求小问题的解,但是问题的推算不同:
|
测试技术 容器
动态规划之背包问题——背包三讲(01背包,完全背包,多重背包)(三)
动态规划之背包问题——背包三讲(01背包,完全背包,多重背包)(三)
214 0
动态规划之背包问题——背包三讲(01背包,完全背包,多重背包)(三)