代码随想录算法训练营第35天 | 46. 携带研究材料、LeetCode416.分割等和子集

目录

46. 携带研究材料

题目描述

输入描述

输出描述

输入示例

输出示例

提示信息

LeetCode416.分割等和子集


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背包问题,找到递归方程dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+values[i]),注意初始化的赋值,以及遍历顺序的选择以及下标的开始位置,这样就不难了。

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N, M;
    cin >> M >> N;
    vector<int> weight(M);//材料所占空间
    vector<int> values(M);//材料所具有的价值
    
    for(int i = 0; i < M; i ++){
        cin >> weight[i];//接收各材料所占空间大小
    }
    for(int i = 0; i < M; i ++){
        cin >> values[i];//接收各材料的价值
    }
    //dp[i][j]表示0到i个物品在空间为j的情况下的最大价值
    vector<vector<int>> dp(M, vector<int>(N + 1, 0));
    for(int i = weight[0]; i < N + 1; i ++){
        dp[0][i] = values[0];//这里在进行初始化
        //只有当背包的容量大于了第一个物品的容量后,才能将其加入
    }
    
    for(int i = 1; i < M; i ++){
        for(int j = 1; j < N + 1; j ++){
            //当容量不够时,就等于上一个物品之前装入的最大值
            if(j < weight[i]) dp[i][j] = dp[i - 1][j];
            //当容量能够装下第i个物品时,需要前一个物品装入的值与
            //能够装下当前物品时的价值作比较取最大
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + values[i]);
        }
    }
    cout << dp[M - 1][N] << endl;
}

当然,可以优化一下空间复杂度,只维护一个一维的滚动数组,每次遍历一个物品即开始更新一遍数组中的值。这里的初始赋值都为0,因为价值都为正数,只要有价值,那么该位置的dp值就不会被初始值0所覆盖,所以是合理的。

当然,这里还需要注意一下遍历的顺序以及起始下标。

只能先遍历物品,然后是背包容量,并且需要倒序遍历

只能先遍历物品是因为如果一旦顺序反过来了,那么就会是背包容量作为外层循环,物品作为内层循环,那么最后的结果就是一个背包容量只放了一个当时比较出来的最大价值的物品,显然结果是不合理的,因此顺序不能反;

其次,为什么需要倒序遍历,因为如果从小下标开始遍历,就会出现后面的元素累加了前面的物品值,比如说weight[0]=1,values[0]=2,那么dp[1]=max(dp[1],dp[0]+values[0])=2,dp[2]=max(dp[2],dp[1]+values[0])=4(这里的初始赋值都为0),这里很明显dp[2]累加了物品0的价值,不合理。所以不能从小下标开始遍历,需要从大小标开始往前走,倒序遍历。会发现倒序遍历避免了这样累加的结果,所以是正确的。

那么为什么二维的时候是正序遍历的呢?其实究其原因就是因为一维数组每一次的变化都会将之前的值覆盖掉,而二维数组没有覆盖值的这一步!

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N, M;
    cin >> M >> N;
    vector<int> weight(M);//材料所占空间
    vector<int> values(M);//材料所具有的价值
    
    for(int i = 0; i < M; i ++){
        cin >> weight[i];//接收各材料所占空间大小
    }
    for(int i = 0; i < M; i ++){
        cin >> values[i];//接收各材料的价值
    }
    //dp[j]表示容量为j时所装物品的最大价值
    vector<int> dp(N + 1, 0);
    //这里在创建dp数组的时候就已经进行了初始值的赋值,都为0
    //因为价值都为正数,所以初始赋值0可以保证在有价值时不会被初始赋值所覆盖
    for(int i = 0; i < M; i ++){
        for(int j = N; j >= weight[i]; j --){//注意这里是倒序
            dp[j] = max(dp[j], dp[j - weight[i]] + values[i]);
        }
    }
    cout << dp[N] << endl;
}

LeetCode416.分割等和子集

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

思路:这里可以采用回溯的方法找到是否存在这样的分割,写起来也比较容易,但是回溯的时间复杂度是指数级别的,很容易就超限了,况且这道题也不是非得用回溯这样的暴力方法求解,可以使用动态规划。

这里我们选择使用一维数组dp[i],表示容量为i时的最大容量。

这里需要注意,我们要找的是分割成两个子集,并且值相等,所以可以知道,如果某个子集和等于了sum/2,那么就说明确实有这样的划分,于是就可以返回true了。

所以我们怎么来找呢?

这里我们采用target=sum/2,如果说有dp[target]=target,那么就可以返回true了,同时需要注意,这里的nums[i]既是物品所占空间,也是物品的价值(将nums中的数组元素看成是物品)。

于是我们知道dp[j]的最大容量就是j,最多也就能装j大小的物品,也就是说dp[j]<=j,因为我们将nums[i]的大小既当作了所占空间大小,也将其作为了物品价值,dp[j]能装的也就只能到顶就是j的大小,在求和过程中可能存在比j大的值,也就是说dp[j]有大于j的值,但是因为本身容量就是j,没有办法将多余j的装进来,所以这部分我们是不管的,只需要初始化dp[target + 1]的大小即可。

所以如果循环遍历结束,有dp[target]=target,那么就可以返回true,因为存在这样的划分。

其实理解dp[j]的大小小于等于j对本题来说是比较关键的,如果说明白了,那这道题也就会理解为什么需要将nums[i]的元素大小作为所占空间的大小同时兼具价值的大小。

    bool canPartition(vector<int>& nums) {
        int sum = 0;//记录总和
        for(int i = 0; i < nums.size(); i ++){
            sum += nums[i];
        }        
        if(sum % 2 == 1) return 0;//和为奇数没有办法将其分为两个和相等的子集
        int target = sum / 2;
        vector<int> dp(target + 1, 0);//dp[i]表示容量为i时能够装下的最大价值
        //注意这里的nums[i]的值既是所占空间大小,也是价值大小
        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;//当dp[target]等于target时,说明有这样的划分,可以使得给定子集划分为两个和相等的子集
        return false;
    }

时间复杂度:O(n^2)

空间复杂度:O(n)

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值