第九章 动态规划part03

携带研究材料(二维DP)

题目描述

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

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

输入描述

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

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

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

输出描述

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

输入示例
6 1
2 2 3 1 5 2
2 3 1 5 4 3
输出示例
5

解题思路

假设我们手中有三份珠宝,对应的重量和价值对应关系如下:

weight134
value152030

在背包最大容量为5的情况下,对题目进行分析:

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

    • dp[i][j]表示前 i 个物品在背包容量为 j 时的最大价值
  2. 确定递推公式

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

      • 如果当前背包容量 j 小于第 i 个物品的重量 weight[i],或是选择不将第i个物品放入背包之中,此时背包的最大价值取决于前i-1个物品的价值所得到的结果:dp[i][j] = dp[i - 1][j]
      • 如果选择将第 i 个物品放入背包,那么背包的容量将减少 weight[i],并且我们需要在前 i-1 个物品得到的最大价值基础上加上第 i 个物品的价值 value[i]
      • 最终,dp[i][j] 取上述两种情况的最大值,得到此时前i个物品在当前背包容量j下得到的最大价值总和。
  3. dp数组初始化

    • 初始化 dp 数组为 0,表示在没有任何物品或背包容量为 0 时的最大价值为 0

    • 对于第一个物品(即 i = 0),如果背包容量 j 大于等于第一个物品的重量 weight[0],则最大价值为第一个物品的价值 value[0],对应代码:

      for (int j = weight[0]; j <= bagWeight; j++) {
          dp[0][j] = value[0];
      }
      

      初始化后的dp表如下所示:image

  4. 确定遍历顺序

    • 以dp[1][3]和dp[1][4]为例,分析在递推公式中dp[i][j]的依赖关系:

      dp[1][3] = max(dp[0][3],dp[0][0]+value[1]) = 20

      dp[1][4] = max(dp[0][4],dp[0][1]+value[1]) = 35

    • 可以发现dp[i][j]依赖于正上方的值,和左上方的值,因此合理的遍历顺序应该为先遍历商品,在遍历背包容量,但是本题也可以先遍历背包再遍历商品,遍历顺序取决于所依赖的值是否被提前赋值了。

  5. 举例推导dp数组

    • 根据上述思路,填补dp数组:

      image

代码实现

测试地址:https://kamacoder.com/problempage.php?pid=1046

#include <bits/stdc++.h>
using namespace std;

int n, 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] 表示前 i 个物品在背包容量为 j 时的最大价值
    vector<vector<int>> dp(weight.size(), vector<int>(bagWeight + 1, 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++) {
            if (j < weight[i])
                // 如果当前背包容量小于第 i 个物品的重量,则不放入该物品
                dp[i][j] = dp[i - 1][j];
            else
                // 否则,比较放入和不放入第 i 个物品的价值,取最大值
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        }
    }
  
    // 输出最终结果,即在背包容量为 bagWeight 时能装入的最大价值
    cout << dp[weight.size() - 1][bagWeight] << endl;
}

int main() {
    // 循环读取输入,直到没有输入为止
    while (cin >> n >> bagWeight) {
        solve();
    }
    return 0;
}

携带研究材料(一维DP)

题目描述

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

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

输入描述

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

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

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

输出描述

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

输入示例
6 1
2 2 3 1 5 2
2 3 1 5 4 3
输出示例
5

解题思路

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

    • dp[j]:表示背包容量为 j 时,可以装入的最大价值。
  2. 确定递推公式

    • 递推公式:dp[j] = max(dp[j], dp[j - costs[i]] + values[i])
    • 在背包容量为 j 的前提下,不装入第 i 个物品(即 dp[j])和装入第 i 个物品(即 dp[j - costs[i]] + values[i],表示背包容量减少,但价值增加)之间,选择一个价值最大的。
  3. dp数组初始化

    • 初始化 dp 数组的长度为 N + 1(背包的最大容量加1),并且初始化所有元素为 0。表示在没有任何物品或背包容量为0时的最大价值为0。
  4. 确定遍历顺序

    • 对于物品的遍历,是正序遍历,对于背包容量的遍历,是逆序遍历
    • 逆序遍历的原因:因为每个物品只能使用一次,为了防止同一个物品被多次选中,我们需要确保在选择第 i 个物品放入背包时,不会影响到其在容量更大的背包中的选择情况。
  5. 举例推导dp数组

    • 通过一维dp,来遍历背包,最终得到结果如下:

      image

代码实现

测试地址:https://kamacoder.com/problempage.php?pid=1046

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

int main() {
    int M, N; // M代表物品的数量,N代表背包的容量
    cin >> M >> N; // 输入物品数量和背包容量

    vector<int> costs(M, 0); // 存储每个物品的成本(或重量)
    vector<int> values(M, 0); // 存储每个物品的价值

    // 输入所有物品的成本和价值
    for (int i = 0; i < M; i++) {
        cin >> costs[i]; // 输入第i个物品的成本
    }

    for (int j = 0; j < M; j++) {
        cin >> values[j]; // 输入第j个物品的价值
    }

    vector<int> dp(N + 1, 0); // 动态规划数组,初始化为0。dp[j]表示在容量为j的背包中能装入物品的最大价值

    // 使用动态规划填表
    for (int i = 0; i < M; i++) { // 遍历所有物品
        for (int j = N; j >= costs[i]; j--) { // 逆序遍历背包容量,确保每个物品只被计算一次
            // 使用递推公式更新dp数组,决定是否将第i个物品加入到背包中
            dp[j] = max(dp[j], dp[j - costs[i]] + values[i]);
        }
    }

    cout << dp[N] << endl; // 输出最大价值,即背包容量为N时的最大价值

    return 0;
}

分割等和子集

题目描述

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

示例 1:

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

示例 2:

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

解题思路

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

    • dp[j] 表示从数组中选取若干个元素,它们的总和最多为 j 时,这些元素的所能达到的最大总和。
  2. 确定递推公式

    • 递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])

    • 公式说明:

      • dp[j]保存了在不超过j这个和的条件下,我们能够达到的最大元素和。

      • nums[i]是当前正在考虑是否加入子集以达到目标和j的元素值。

      • dp[j - nums[i]]表示在我们选择包含当前元素nums[i]之前,达到剩余和j - nums[i]的最大值。这个值加上nums[i]就等于如果包括当前元素,我们能达到的一个新的和值。

      • max(dp[j], dp[j - nums[i]] + nums[i])比较了两种情况:一种是不包括当前元素nums[i]时已经可以达到的最大和(dp[j]),另一种是如果包括当前元素nums[i],能够达到的新的可能最大和(dp[j - nums[i]] + nums[i])。从这两个值中选取最大值作为新的dp[j]

  3. dp数组初始化

    • 因为题目的取值范围给出了数组之和最大值为20000,因此可以初始化目标和为10001,并初始化dp数组所有值为0,表示没有选择任何元素时,所有可能的总和都是0,也可以确保在遍历的过程中初始值不会被覆盖
  4. 确定遍历顺序

    • 遍历数组元素的时候采用正序遍历
    • 在计算当前元素能到达的最大和的时候采用倒序遍历,以确保每个元素只使用了一次
  5. 举例推导dp数组

    • 以用例1,输入[1,5,11,5]为例,推导dp数组,如下所示:

      image

代码实现

测试地址

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int targetSum = 0;
        for (int num : nums) { // 计算数组元素总和
            targetSum += num;
        }
        if (targetSum % 2 == 1) // 如果总和为奇数,则不能平分为两个和相等的子集
            return false;

        int target = targetSum / 2; // 目标和为数组总和的一半

        vector<int> dp(10001,0); // 创建动态规划数组,用于存储可能达到的各个和的最大值
        for (int i = 0; i < nums.size(); i++) { // 遍历数组中的每个数
            for (int j = target; j >= nums[i];
                 j--) { // 逆序遍历,从target到nums[i]
                // 更新dp[j]为当前和j和减去nums[i]再加上nums[i](即选择当前数字)的最大值
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        if (dp[target] ==
            target) { // 如果dp[target]等于target,则表示可以分割为两个和相等的子集
            return true;
        }
        return false;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值