24暑假算法刷题 | Day35 | 动态规划 III 背包问题 I | 卡码网46. 携带研究材料,LeetCode 416. 分割等和子集


卡码网46. 携带研究材料

点此跳转题目链接

题目描述

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

小明的行李空间为 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 <= 5000
1 <= M <= 5000
研究材料占用空间和价值都小于等于 1000

题解

标准的 01背包问题 ,动态规划解决。

💡 01背包基础参考:代码随想录-01背包理论基础

  • dp 数组含义: dp[i][j] 表示从下标 0i 的物品中选择,放入容量为 j 的背包中,所能达到的最大价值
  • 状态转移方程:
    • 如果当前物品的重量/所占空间 weights[i] 大于当前背包容量 j ,则不放入,即 dp[i][j] = dp[i - 1][j]
    • 否则,比较放入物品 i (可能需要拿出其他物品腾空间)和不放入 i 哪个价值更高,即 dp[i][j] = max(dp[i - 1][j], dp[i - i][j - weights[i]] + values[i])

代码(C++)

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    int m, n;
    std::cin >> m >> n;
    std::vector<int> weights(m), values(m);
    for (int i = 0; i < m; ++i)
        std::cin >> weights[i];
    for (int i = 0; i < m; ++i)
        std::cin >> values[i];

    std::vector<std::vector<int>> dp(m, std::vector<int>(n + 1, 0));
    for (int j = weights[0]; j <= n; ++j)
        dp[0][j] = values[0];
    for (int i = 1; i < m; ++i) {
        for (int j = 0; j <= n; ++j) {
            if (weights[i] > j)
                dp[i][j] = dp[i - 1][j];
             else 
                dp[i][j] = std::max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]);
        }
    }
    std::cout << dp[m - 1][n] << std::endl;

    return 0;
}

或者也可以将 dp 数组改为滚动数组,实现数组降维,减小空间开销(参考:代码随想录 )

// 一维dp
std::vector<int> dp(n + 1);
for (int j = weights[0]; j <= n; ++j)
    dp[j] = values[0];
for (int i = 1; i < weights.size(); ++i) {
    for (int j = n; j >= weights[i]; --j) 
        dp[j] = std::max(dp[j], dp[j - weights[i]] + values[i]);
}
std::cout << dp[n] << std::endl;

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

题解

根据题目要求,首先不难想到一个基本目标:每个子集的元素之和应该恰为 nums 所有元素之和的 一半 。自然,我们可以先找到这个目标和:

// 先求出每个子集的元素和
int sum = 0;
for (int num : nums)
    sum += num;
if (sum % 2 == 1)
    return false; // 目标和为奇数肯定无法实现
sum /= 2;

接下来就是看 nums 中能不能找一堆元素“凑”出 sum 。这样考虑似乎和 组合问题类似,于是尝试回溯算法解决:

class Solution // 回溯算法:超时
{
private:
    int curSum = 0;
    bool flag = false;
    void backTracking(const vector<int> &nums, int target, int start) {
        // 剪枝
        if (curSum > target)
            return;
        // 递归出口:子集元素和达到目标值
        if (curSum == target) {
            flag = true;
            return;
        }
        for (int i = start; i < nums.size(); ++i) {
            curSum += nums[i]; // 处理
            backTracking(nums, target, i + 1); // 递归
            curSum -= nums[i]; // 回溯
        }
    }

public:
    bool canPartition(vector<int> &nums)
    {
        // 先求出每个子集的元素和
        int sum = 0; 
        for (int num : nums)
            sum += num;
        if (sum % 2 == 1)
            return false; // 目标和为奇数肯定无法实现
        sum /= 2;

        // 回溯算法
        sort(nums.begin(), nums.end());
        backTracking(nums, sum, 0);
        return flag;
    }
};

经检验,回溯算法可行但是数据规模大的时候会超时——毕竟回溯本质上是穷举。

于是我们考虑更优雅的算法:动态规划。有一定 01背包问题基础 的话可以发现,本题其实可以转化为一个01背包问题:

能否将一个容量为 sum 的背包,用 weightsvalues 均为 nums 的物品恰好装满?

“恰好装满容量为 sum 的背包” 对应 “找到一个元素和为 sum 的子集”,“ weightsvalues 均为 nums ” 对应 “从 nums 中取子集”。

那么就按经典的01背包问题套路解决即可:

bool canPartition(vector<int> &nums)
{
    // 先求出每个子集的元素和
    int sum = 0;
    for (int num : nums)
        sum += num;
    if (sum % 2 == 1)
        return false; // 目标和为奇数肯定无法实现
    sum /= 2;

    // 背包问题:能否将weights和values都为nums、容量为sum的背包恰好装满
    vector<vector<int>> dp(nums.size(), vector<int>(sum + 1, 0));
    for (int j = nums[0]; j <= sum; ++j)
        dp[0][j] = nums[0];
    for (int i = 1; i < nums.size(); ++i) {
        for (int j = 0; j <= sum; ++j) {
            if (nums[i] > j)
                dp[i][j] = dp[i - 1][j];
            else
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);
        }
    }
    return dp[dp.size() - 1][sum] == sum;
}

当然也可以将 dp 数组从二维降为一维( 滚动数组 ):

bool canPartition(vector<int> &nums)
{
    // 先求出每个子集的元素和
    int sum = 0;
    for (int num : nums)
        sum += num;
    if (sum % 2 == 1)
        return false; // 目标和为奇数肯定无法实现
    sum /= 2;

    // 背包问题:能否将weights和values都为nums、容量为sum的背包恰好装满
    vector<int> dp(sum + 1, 0);
    for (int j = nums[0]; j <= sum; ++j)
        dp[j] = nums[0];
    for (int i = 1; i < nums.size(); ++i) {
        for (int j = sum; j >= nums[i]; --j)  // 注意:一维dp要倒着遍历容量j
            dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
    }
    return dp[sum] == sum;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值