[leedcode]刷题有感--动态规划经典问题--01背包问题

一、01背包问题理论介绍

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。类似于这种类型的就是经典的01背包问题。

对于01背包问题,我们通常使用动态规划来求解,当然也可以用回溯算法来求解,但是回溯算法的时间复杂度为 指数级别,若题目中的测试数据过大,则可能会超时。

二、题目示例

示例一:


46. 携带研究材料(第六期模拟笔试)

时间限制:5.000S  空间限制:128MB

题目描述

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

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

输入描述

第一行包含两个正整数,第一个整数 M 代表研究材料的种类,第二个正整数 N,代表小明的行李空间。

第二行包含 M 个正整数,代表每种研究材料的所占空间。 

第三行包含 M 个正整数,代表每种研究材料的价值。

输出描述

输出一个整数,代表小明能够携带的研究材料的最大价值。

输入示例
6 1
2 2 3 1 5 2
2 3 1 5 4 3
输出示例
5
提示信息

小明能够携带 6 种研究材料,但是行李空间只有 1,而占用空间为 1 的研究材料价值为 5,所以最终答案输出 5。 

数据范围:
1 <= N <= 1000
1 <= M <= 1000
研究材料占用空间和价值都小于等于 1000

解析--动态规划五部曲


对于动态规划类问题,我们使用动态规划五部曲来进行思考。


第一步、构造动态规划dp数组

vector>dp(m, vector(n + 1, 0));  //构造dp数组

我们在构造dp数组的时候,一定要想清楚dp[i][j]的具体含义。

dp[i][j]代表-->从0~i的物品中任取,放进容量大小为j的背包中,所获得的最大价值。


第二步、明确递推公式

if(space[i] > j) {
   dp[i][j] = dp[i - 1][j];
 }
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - space[i]] + val[i]);

在space[i] > j即物品i的占用空间大于目前背包的容量时,从0~i的物品中任取,放进容量大小为j的背包中所获得的最大价值 就等于 从0~i - 1的物品中任取,放进容量大小为j的背包中,所获得的最大价值。

在space[i] < j即物品i的占用空间小于目前背包的容量时,我们从两种情况中取最大值,因为要计算的是所获得的最大价值。

  • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

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


第三步、dp数组初始化

//初始化dp数组,对背包容量为0时所存储的最大价值进行初始化--都为0
    for(int i = 0; i < m; i++) {
        dp[i][0] = 0;
    }
    
    for(int i = 0; i <= n; i++) {
        if(space[0] <= i) {
            dp[0][i] = val[0];
        }
    }

首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。

再看其他情况。

状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。


第四步、遍历dp数组

for(int i = 1; i < m; i++) {  //研究材料
        for(int j = 1; j <= n; j++) {  //背包容量
            if(space[i] > j) {
                dp[i][j] = dp[i - 1][j];
            }
            else
            dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - space[i]] + val[i]);
        }
    }

在本题所使用的二维动归中,先遍历材料再遍历背包,亦或者先遍历背包再遍历材料都是可以的。

都会遍历完整个dp数组。自己手动画一下dp数组的图标是最直观的。


第五步、手动模拟 + 举例推导

[leedcode]刷题有感--动态规划经典问题--01背包问题_第1张图片

 


最后呈上完整代码

#include
using namespace std;
int main() {
    int m, n;
    cin>>m>>n;
    vectorspace(m);  //每种材料的所占空间
    vectorval(m);  //每种材料的研究价值
    for(int i = 0; i < m; i++) {
        cin>>space[i];
    }
    for(int i = 0; i < m; i++) {
        cin>>val[i];
    }
    vector>dp(m, vector(n + 1, 0));  //构造dp数组
    
    //初始化dp数组,对背包容量为0时所存储的最大价值进行初始化--都为0
    for(int i = 0; i < m; i++) {
        dp[i][0] = 0;
    }
    
    for(int i = 0; i <= n; i++) {
        if(space[0] <= i) {
            dp[0][i] = val[0];
        }
    }
    
    for(int i = 1; i < m; i++) {  //研究材料
        for(int j = 1; j <= n; j++) {  //背包容量
            if(space[i] > j) {
                dp[i][j] = dp[i - 1][j];
            }
            else
            dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - space[i]] + val[i]);
        }
    }
    
    cout<

思路二-- 将二维动态规划 压缩为 一维动态规划 -- 滚动数组

#include
using namespace std;
int main() {
   int m, n;
   cin>>m>>n;
   vectorspace(m);
   vectorval(m);
   vectordp(n + 1);
   for(int i = 0; i < m; i++) {
       cin>>space[i];
   }
   for(int i = 0; i < m; i++) {
       cin>>val[i];
   }
   for(int i = 0; i < m; i++) {
       for(int j = n; j >= space[i]; j--) {
           dp[j] = max(dp[j], dp[j - space[i]] + val[i]);
       }
   }
   cout<

1、一维dp数组的含义

dp[j] 表示容量大小为 j 的背包所能装的物品最大价值。

2、递推公式

利用滚动数组,新的dp[j]将旧的dp[j]覆盖的方式不断更新dp[j]。dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。

dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值。

3、初始化

dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。

那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?

看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

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

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

那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。

4、遍历方式

将二维dp压缩为一维dp后,遍历顺序是固定的,必须倒序遍历背包空间,如果正序遍历,会出现同一个物品被连续放进背包两次的情况,请自己手动模拟一下。

那为什么for循环的内外嵌套顺序不能修改呢?因为如果嵌套顺序互换,每次背包中只会装进去一个物品,不符合题意。


示例二: ---01背包算法应用

分割等和子集

题目难易:中等

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

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

本题为典型的01背包问题。我们可以对数组大小为偶数的数组进行求和,取其二分之一,只要在集合中可以找到和为 sum / 2 的子集,那么这个集合就可以被拆为两个子集。

我们可以将条件转化为01背包问题的条件。

即 元素的价值 和 元素所占空间 大小相等。我们要将元素放进容量大小为 sum / 2的背包中,如果背包中所装元素的大小之和 的最大值 为 sum / 2,那么我们返回true,如果不是,则返回 false。


呈上代码:

class Solution {
public:
    int sum = 0;
    bool canPartition(vector& nums) {
        if(nums.size() < 2) return false;
        for(auto num : nums) {
            sum += num;
        }
        if(sum % 2 != 0) return false;
        int target = sum / 2;
        vector>dp(nums.size(), vector(target + 1, 0));  //构造二维dp数组
        vectorspace(nums.size());
        vectorval(nums.size());
        for(int i = 0; i < nums.size(); i++) {
            space[i] = nums[i];
            val[i] = nums[i];
        }
        for(int i = 0; i <= target; i++) {
            if(space[0] <= i) {
                dp[0][i] = val[0];  //初始化dp数组
            }
        }
        for(int i = 1; i < nums.size(); i++) {
            for(int j = 1; j <= target; j++) {
                if(space[i] > j) {
                    dp[i][j] = dp[i - 1][j];
                }
                else {
                    dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - space[i]] + val[i]);
                }
            }
        }
        if(dp[nums.size() - 1][target] == target) return true;
        return false;
    }
};

你可能感兴趣的:(动态规划,算法)