首页 > 编程语言 >代码随想录算法训练营第四十一天|01背包问题, 01背包问题—— 滚动数组,分割等和子集

代码随想录算法训练营第四十一天|01背包问题, 01背包问题—— 滚动数组,分割等和子集

时间:2024-03-09 17:12:38浏览次数:40  
标签:01 weight nums int 随想录 背包 遍历 dp

01背包问题,你该了解这些! 

题目链接:46. 携带研究材料(第六期模拟笔试) (kamacoder.com)

思路:第一次遇到背包问题,好好记住吧。代码随想录 (programmercarl.com)

#include<bits/stdc++.h>
using namespace std;
int main(){
    int m,n;
    cin>>m>>n;
    vector<int>z(m);
    vector<int>value(m);
    for(int i=0;i<m;i++){
        cin>>z[i];
    }
    for(int i=0;i<m;i++){
        cin>>value[i];
    }
    
    vector<vector<int>> dp(m,vector<int>(n+1,0));
    for(int i=z[0];i<=n;i++){
        dp[0][i]=value[0];
    }
    
    for(int i=1;i<m;i++){
        for(int j=0;j<=n;j++){
            if(j<z[i])dp[i][j]=dp[i-1][j];
            else dp[i][j]=max(dp[i-1][j],dp[i-1][j-z[i]]+value[i]);
            
        }
    }
    cout<<dp[m-1][n];
}

01背包问题,你该了解这些! 滚动数组  

滚动数组,说白了就是将上一题中的二维数组压缩成一维数组。dp数组的更新方式变为dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

但有一点需要注意,这里我直接引用官网原文:

这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

为什么呢?

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

同时,只能先遍历物品,嵌套倒序遍历背包容量,不能反着来。

void test_1_wei_bag_problem() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    // 初始化
    vector<int> dp(bagWeight + 1, 0);
    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] << endl;
}

分割等和子集 

题目链接:416. 分割等和子集 - 力扣(LeetCode)

思路:首先来看我最初的思路,先将数组排序,然后设一个堆栈来记录我选择了哪些元素。从小到大遍历数组,同时累加压入堆栈的元素,如果累加值过大于目标值,则将栈顶弹出同时继续尝试加入该元素,否则更新累加值并加入堆栈(注意此时可以return的情况)。实际上,这是一种贪心解法,并不能解决这个问题,具体来说,是因为下一个元素并不能决定这个元素是否该留在这个堆栈中,而是该元素之后的所有元素共同决定这个元素能否留在堆栈中(这应该就是贪心和DP的区别所在吧)。下面是错误做法的核心部分(与前文略有差异,该做法从大到小遍历数组):

        r.push(nums.back());
        int z = nums.size() - 1;
        while (!z) {
            for (int j = z; j >= 0; j--) {
                if (sum - nums[j] > 0) {
                    sum -= nums[j];
                    r.push(nums[j]);
                } else if (sum - nums[j] < 0 && (!r.empty())) {
                    r.pop();
                    j++;
                } else if ((sum - nums[j]) == 0) {
                    return true;
                }
            }
            z--;
        }

该方法不能在走投无路的时回溯到错误还没有发生的时刻,也就不能解决问题。不得不说,这道题让我对贪心和DP的理解更加深入了。

正确做法如下:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;

        // dp[i]中的i表示背包内总和
        // 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
        // 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
        vector<int> dp(10001, 0);
        for (int i = 0; i < nums.size(); i++) {
            sum += nums[i];
        }
        // 也可以使用库函数一步求和
        // int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum % 2 == 1) return false;
        int target = sum / 2;

        // 开始 01背包
        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]);
            }
        }
        // 集合中的元素正好可以凑成总和target
        if (dp[target] == target) return true;
        return false;
    }
};

 

标签:01,weight,nums,int,随想录,背包,遍历,dp
From: https://www.cnblogs.com/Liubox/p/18062977

相关文章

  • 一本通 1270 混合背包 题解
    是在hydro上做的,墙裂推荐hydro的一本通题库!链接是:https://hydro.ac/d/ybttk/p/T1270。分析一下,可以分类讨论,处理的时候,零一背包(\(p_i=1\))一个,完全背包(\(p_i=0\))一个,多重背包(\(p_i>1\))一个,状态转移方程不用列的,直接去抄模板就可以啦~代码是这样的捏:#include<bits/st......
  • [THUSC2016] 补退选
    首先对于所有的字符串进行哈希构建两个哈希表,均为哈希值映射至vector我们约定一些东西方便表示\(v1\)表示第一个哈希表对应的vector,\(v2\)表示第二个哈希表对应的vector\(v1\)中元素表示当前该前缀对应所有操作编号(可能不正确,但是没影响,具体看下面的注意标注的地方)第二......
  • 代码随想录 第十六天 | ● 104.二叉树的最大深度 559.n叉树的最大深度 ● 111.二叉树
    leetcode:104.二叉树的最大深度-力扣(LeetCode)思路:递归判断每次左右节点的是否存在,存在自然加一,return的1就是这样,判断子节点的左右两端是否有节点,统计有的节点数量,也就是左右的高度classSolution{publicintmaxDepth(TreeNoderoot){//后序遍历if......
  • 01_Ubuntu启用root用户
    Ubuntu启用root用户1命令行的组成:topeet:当前操作用户Ubuntu:代表主机名~:当前目录名$:代表不是root用户:代表root用户权限2为什么要启用root用户?我们使用Ubuntu系统主要用来做嵌入式开发,不是linux运维,没有必要对root用户过于敏感。系统的权限都要为我们嵌入式开发人......
  • [BUUCTF 2018]Online Tool 1
    [BUUCTF2018]OnlineTool1<?phpif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {    $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];}if(!isset($_GET['host'])) {    highlight_file(__FILE__)......
  • 01.基于javaEE_大学生就业信息管理系统源码
    基于javaEE_大学生就业信息管理系统:本系统分系统管理员,教师用户,企业用户和毕业生用户4个用户角色。**系统管理员主要功能有系别管理、专业管理、老师管理员管理、站内新闻管理、企业用户管理、岗位管理、文档管理、公告管理、留言管理、就业查询统计(包括就业情况查询,区域分布统......
  • 代码随想录算法训练营第四天| 24. 两两交换链表中的节点 19.删除链表的倒数第N个节点
    24.两两交换链表中的节点https://leetcode.cn/problems/swap-nodes-in-pairs/description/publicListNodeswapPairs(ListNodehead){if(head==null||head.next==null)returnhead;ListNoderes=head.next;ListNodepre=newListNod......
  • P4765 CERC2014 The Imp
    设我们打算买的\(K+1\)个物品为\(p_1,p_2,\cdots,p_{K+1}\)。则收益为\(min(v_{p_1}-c_{p_1},v_{p_2}-c_{p_1}-c_{p_2},\cdots,v_{p_{K+1}}-\sum_{i=1}^{K+1}c_{p_i})\)。从邻项交换的角度,考虑我们按哪种顺序购买这\(K+1\)个物品收益最大。任取相邻两项\((v_{p_x},c_{p_......
  • 代码随想录算法训练营day17 | leetcode 110. 平衡二叉树、257. 二叉树的所有路径、404
    目录题目链接:110.平衡二叉树-简单题目链接:257.二叉树的所有路径-简单题目链接:404.左叶子之和-简单题目链接:110.平衡二叉树-简单题目描述:给定一个二叉树,判断它是否是平衡二叉树示例1:输入:root=[3,9,20,null,null,15,7]输出:true示例2:输入:root=[1,2,2,3,3,null,nul......
  • Mysql 学习记录 #01
    Mysql学习记录#01表的基本操作--创建表CREATETABLEIFNOTEXISTS`student`( `id`INT(4)NOTNULLAUTO_INCREMENTCOMMENT'编号', `name`VARCHAR(30)NOTNULLDEFAULT'匿名'COMMENT'姓名', `pwd`VARCHAR(20)NOTNULLDEFAULT'123456�......