代码随想录算法训练营第四十二天| 二维背包、一维背包、LeetCode 416.分割等和子集

news2024/11/27 14:41:34

一、二维背包

文章讲解/视频讲解:https://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html

状态:已解决

1.背包问题介绍

        背包问题实则是一类问题的集合,有好多不同小类型,但本质还是一样的,基本组成都是:一个容量固定的背包、若干件物品(拥有体积、价值、数量三种属性),根据物品的数量的限制,可以将背包问题划分为以下几类:

        背包问题是动态规划的经典问题,也是难点所在,一般来说,学会01背包和完全背包就够做题了,再往上就是竞赛级别的了。而完全背包实则也是在01背包的基础上进行的变化升级,因此,01背包是重中之重,我们必须掌握。

2.01背包问题

        标准题意:有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

         这种取或不取的问题,我们自然容易想到用回溯法遍历所有情况暴力去做,但这种方法时间复杂度太高(指数级),在有更优化的解法时不建议使用。

        这里说的更优化的解法实则就是动态规划。具体怎么做我们利用动规五部曲来分析。

(1)确定dp数组以及下标含义:

        我们知道动规中dp数组是用于表示状态的一个数组,那我们这道题有哪些状态呢?首先,我们需要一个状态说明此刻该抉择选取哪个物品了,另外我们需要一个状态来说明此时背包的容量。那么,我们就可以定义一个二维数组dp。dp[i][j] 表示从下标为[0-i]的物品里任意取(0 <= k <=i,可能取了物品k也可能没取物品k),放到容量为j的背包,价值总和最大是多少。画出dp数组如图:

(2)确定递推公式:

        我们知道了dp[i][j]的含义:抉择第i个物品时,背包重量为j,那么结合逻辑,我们就可以得出状态dp[i][j]的来源:

  • 不放物品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的最大价值,那么在容量为 j 时可以选择多放入物品 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]);

(3)dp数组初始化:

        首先从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[i][j]都是由左上角或者上面推导过来的,因此初始化就无所谓,反正都会被覆盖,为了方便,统一初始化为0。

(4)遍历顺序:

        我们知道dp是一个二维数组,也就是有两个维度:物品和背包容量,那么我们在进行循环的时候哪个维度放外层哪个维度放内层呢?我们根据递推公式可以知道一个元素是从左上和上面推出来的,也就是说,我们将物品作为外层,背包容量作为内层的话,那么从小到大遍历dp数组就是一排一排填满的,如果将背包容量作为外层,物品作为内层的话,那么dp数组就是一列一列的填满的。二者都可以顺利计算完整个dp数组(只要每排或每列是从小到大遍历的,那么我们做下一排或者下一列时,无论如何都能得到上面一格或者左上一格的元素)。

(5)举例推导dp数组:

        按现在的逻辑得到dp数组如下:

        最终结果就是dp[2][4]。 

3.具体实现 

        卡码网46题就是一个经典的01背包问题,我们根据上述分析可以给出代码:

#include<iostream>
#include<vector>
using namespace std;
int main(void){
    int m,n;
    cin>>m>>n;
    vector<int> weight(m,0);
    vector<int> value(m,0);
    vector<vector<int>> dp(m,vector<int>(n+1,0));
    for(int i=0;i<m;i++){
        cin>>weight[i];
    }
    for(int i=0;i<m;i++){
        cin>>value[i];
    }
    for(int i=weight[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-weight[i]>=0)
            	dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
            else
            	dp[i][j] = dp[i-1][j];
        }
    }
//    for(int i=1;i<m;i++){
//        for(int j=0;j<=n;j++){
//            cout<<dp[i][j]<<" ";
//        }
//        cout<<endl;
//    }
    cout<<dp[m-1][n];
}

二、一维背包 

文章讲解/视频讲解:https://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-2.html

状态:已解决

1.滚动数组 

        一维背包实则还是01背包问题,只是我们将状态数组从二维降到了一维。依据是什么呢?我们根据二维背包的分析可以得知二维dp数组的每一层的每一个格子(除初始化的最左侧和最上方)实则都是从上一层的左上角格子和正上方格子推导出来的,也就是说,每一层的值只跟上一层的值有关,跟这层的值没有关系。也就是说,我们只需要保存上一层的状态,然后推导出这一层的状态后,舍弃上一层状态,更新为新推导出的这一层状态,由此再去推导下一层。即,整个二维数组被压缩成一行(层),然后不断地向下滚动着更新,故称之为滚动数组。由于数组降维,从两个状态变为了一个状态,故这种优化方法也被称为状态压缩。

(1)确定dp数组及下标含义:

在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。

(2)一维dp数组的递推公式:       

  我们知道二维dp数组的递推公式是:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]),i是物品标号也是层数标号,当层数被压缩为一层时,我们知道当前一维数组实际就是推导完的上层状态,那么原先上层的dp[i - 1][j], dp[i - 1][j - weight[i]],实际就是现在的dp[j]、dp[j-weight[i]]。故递推公式为:

dp[j] = max(dp[j], dp[j - weight[i]] + value[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数组的初始化:
        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数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了

(4)一维dp数组的遍历顺序:       

        假如现在外层是依次遍历物品。那么,内层的背包容量也还是从小到大来遍历吗?漏!!!我们知道,内层循环现在是背包容量,由于现在的dp[ j ]是由dp[j]和dp[j-weight[i]]决定的,那么我们推导dp[j]时,本身需要的是上一层在 j 和 j-weight[i]时的值,而如果每次对于物品i,都从小到达遍历容量的话,那么在算这层的 j容量 之前时,它前面的容量值((比如j - weight[i]))就已经被更新成这层的推算值,而不是上层的推算值了。因此,我们只会反复根据第一层物品的价值和重量进行推导,而不是这层物品的相关值。

        例:物品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,被放入了两次,所以不能正序遍历。

        为什么倒序遍历,就可以保证物品只放入一次呢?因为倒序从当层的末尾容量开始推导,那么只动了j容量后面的值,j前面的值仍然为上层的推算值,由此推得的dp[j] 也就是由上层推算值推导出来的正确的dp[j]。倒序就是先算dp[2]dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)dp[1] = dp[1 - weight[0]] + value[0] = 15。所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

       再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?不可以!因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。

(5)举例推导dp数组:

2.代码实现 

        还是以卡码网46题为例。

#include<iostream>
#include<vector>
using namespace std;
int main(void){
    int m,n;
    cin>>m>>n;
    vector<int> weight(m,0);
    vector<int> value(m,0);
    vector<int> dp(n+1,0);
    for(int i=0;i<m;i++){
        cin>>weight[i];
    }
    for(int i=0;i<m;i++){
        cin>>value[i];
    }
    for(int i=0;i<m;i++){
        for(int j=n;j>=0;j--){
            if(j-weight[i]>=0)
            	dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
            else
            	dp[j] = dp[j];
        }
    }
//    for(int i=1;i<m;i++){
//        for(int j=0;j<=n;j++){
//            cout<<dp[i][j]<<" ";
//        }
//        cout<<endl;
//    }
    cout<<dp[n];
}

三、LeetCode 416.分割等和子集

题目链接/文章讲解/视频讲解:https://programmercarl.com/0416.%E5%88%86%E5%89%B2%E7%AD%89%E5%92%8C%E5%AD%90%E9%9B%86.html

状态:已解决

1.思路 

        这道题不难,关键在于如何将问题转换为01背包问题。题眼:将这个数组分割成两个子集,使得两个子集的元素和相等。我们知道,给定了数组,那么数组的和sum就是确定的,也就是说,我们现在的目标就是找一个集合使得集合元素之和等于sum/2,那么剩余元素构成的集合的和也就为sum/2了。现假设数组元素个数为m,那么套用到背包模型中,就是将m个物品(重量、价值均为nums[i]),装到容量为sum/2的背包中去,看是否能够装满背包。

        那么怎么判断是否能够装满背包呢?看背包最多能装的总价值是否刚好等于sum/2,即dp[target] == target。理由:

        (1)dp[target] > target 的情况不可能出现,因为现在一个物品的价值等于这个物品的重量,装满一个背包最多价值等于重量,不可能出现最终价值超过该背包容量的情况。

        (2)dp[target] < target:说明尽量装背包装不到tagret,也就是装不满背包,故不可能凑到某个集合的元素之和等于sum/2。

2.代码实现

        直接在上面的代码上做修改就行:m = num.size(),n = sum/2,weight[i] = nums[i],value[i] = nums[i]。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(int i=0;i<nums.size();i++){
            sum += nums[i];
        }
        int m = nums.size(),n = sum/2;
        if(sum % 2 != 0) return false;//此处可剪枝
        vector<int> dp(n+1,0);
        for(int i=0;i<m;i++){
            for(int j=n;j>=0;j--){
                if(j-nums[i]>=0)
                    dp[j] = max(dp[j],dp[j-nums[i]]+nums[i]);
                else
                    dp[j] = dp[j];
            }
        }
        if(dp[n] == n) return true;
        else return false;
    }
};

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

空间复杂度:O(n)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1599645.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux_iptables防火墙学习笔记

文章目录 iptables 概述四表五链iptables 安装启动iptables 配置详解iptables配置文件iptables配置语法iptables常用实例查看规则修改默认规则保存和备份规则恢复备份的规则清空规则放行SSH服务在ubuntu14.04中iptables规则持久化 iptables 概述 主机型 对主机进行保护 网络型…

Element-Ui的Form表单:Label文本两端对齐,且必填项的*不影响布局

1. HTML 结构 首先&#xff0c;确保你的 HTML 或 Vue 模板中有一个 el-form 组件&#xff0c;类似下面这样&#xff1a; <div id"app"><el-form :model"form" label-width"100px"><el-form-item label"用户名">&l…

Linux系统(centos,redhat,龙芯,麒麟等)忘记密码,怎么重置密码

Linux系统&#xff08;centos,redhat,龙芯&#xff0c;麒麟等&#xff09;忘记密码&#xff0c;怎么重置密码&#xff0c;怎么设置新的密码 今天在操作服务器时&#xff0c;DBA忘记了人大金仓数据库的kingbase密码&#xff0c;他的密码试了好多遍&#xff0c;都不行。最后只能…

JCVI-筛选blast最佳结果(生物信息学工具-015)

通常&#xff0c;大家会问我们经过了NR注释&#xff0c;SwissProt注释&#xff0c;那么如何进行&#xff0c;如何挑选最佳比对结果&#xff1f; 同理&#xff0c;存在一个问题&#xff0c;如何挑选最佳的blast比对结果&#xff1f;什么事最优的同源序列&#xff1f; 唐海宝老…

jenkins构建微信小程序并展示二维码

测试小程序的过程中&#xff0c;很多都是在回头和前端开发说一句&#xff0c;兄弟帮我打一个测试版本的测试码&#xff0c;开发有时间的情况下还好&#xff0c;就直接协助了&#xff0c;但是很多时候他们只修复了其中几个bug&#xff0c;其他需要修复的bug代码正在编写&#xf…

ENSP-旁挂式AC

提醒&#xff1a;如果AC不能成功上线AP&#xff0c;一般问题不会出在AC上&#xff0c;优先关注AC-AP线路上的二层或三层组网的三层交换机 拓扑图 管理VLAN&#xff1a;99 | 业务VLAN&#xff1a;100 注意点&#xff1a; 1.连接AP的接口需要打上pvid为管理vlan的标签 2.AC和…

引导和服务(2)

服务 1.systemd服务的简要介绍 &#xff08;1&#xff09;对比5 6 可以解决依赖关系并行启动 &#xff08;2&#xff09;按需启动 &#xff08;3&#xff09;自动解决依赖关系 负责在系统启动或运行时&#xff0c;激活系统资源&#xff0c;服务器进程和其它进程 2.System…

React-hooks:useReducer初始化函数 和 初始值 区别

useReducer 用法&#xff1a; const [state, dispatch] useReducer(reducer, initState, init?);其中&#xff0c;initialArg 为初始值&#xff08;必传&#xff09;&#xff0c;init 为初始函数&#xff08;可选&#xff09;。 当没有 init 参数时&#xff0c;state的初始…

Day56 动态规划 part16

Day56 动态规划 part16 583. 两个字符串的删除操作 我的思路&#xff1a; 感觉跟前两天子序列差不多&#xff0c;但是又有差别 这次是求删减最小次数&#xff0c;状态转移方程是比小 另外要注意初始化&#xff0c; 当i 0时&#xff08;word2为空&#xff09;&#xff0c;wor…

全球最新国内外18个热门风景视频素材网站推荐

寻找最新的高清风景视频素材&#xff1f;这里有国内外共18个热门网站&#xff0c;精心整理供您选择。 国内资源&#xff1a; 蛙学网&#xff1a;免费提供多种无版权视频素材&#xff0c;资源丰富。新GG网&#xff1a;需QQ登录&#xff0c;提供丰富的视频模板&#xff0c;通过…

从零开始写 Docker(十一)---实现 mydocker exec 进入容器内部

本文为从零开始写 Docker 系列第十一篇&#xff0c;实现类似 docker exec 的功能&#xff0c;使得我们能够进入到指定容器内部。 完整代码见&#xff1a;https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实现有一个大致认识&#xff1a; 核心原理&…

Java --- 类与对象

上篇内容给大家带来了Java的语句与数组的相关内容&#xff0c;那么本期内容比较重要&#xff0c;需要读者们掌握Java面向对象编程的根本&#xff0c;通过这篇博客来让读者浅入理解Java类的一些基本操作。 目录 一.特点&#xff1a; 二.成员变量&#xff1a; 三.访问修饰符&a…

工作流引擎项目解析

API 编辑 在Camunda中&#xff0c;API的继承关系主要体现在各个服务接口之间。以下是Camunda中一些常见服务接口的继承关系&#xff1a; ProcessEngineServices 接口&#xff1a; RepositoryService&#xff1a; 负责管理流程定义和部署。 RuntimeService&#xff1a; 负责管…

微信人脉扩张!多号批量自动加好友,你get到了吗?

微信是我们在拓展社交圈和寻找商业机会时&#xff0c;与更多的人建立联系的重要渠道。但是&#xff0c;手动一个个添加好友显然费时费力&#xff0c;这时候&#xff0c;微信管理系统的批量自动加好友功能就成为了微信人脉扩张的神器。 通过微信管理系统&#xff0c;我们可以轻…

思维导图ai生成软件分享5款好用的!

思维导图ai生成软件分享5款好用的&#xff01; 在快节奏的信息时代&#xff0c;思维导图作为一种有效的思维整理工具&#xff0c;越来越受到人们的青睐。它能够将复杂的思维过程可视化&#xff0c;帮助我们更好地梳理思路、规划工作。近年来&#xff0c;随着人工智能技术的飞速…

【笔记】探索生成范式:大型语言模型在信息提取中的作用

探索生成范式&#xff1a;大型语言模型在信息提取中的作用 摘要介绍 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字篆刻时光&#xff0c;感谢你的陪伴与支持 ~ &#x1f680; 欢迎一起踏上探险之旅&#xff0c;挖掘无限可能&#xff0c;共同成长&am…

实验六 智能手机互联网程序设计(微信程序方向)实验报告

实验目的和要求 请完成创建图片库应用&#xff0c;显示一系列预设的图片。 提供按钮来切换显示不同类别的图片。 二、实验步骤与结果&#xff08;给出对应的代码或运行结果截图&#xff09; 1.WXML <view> <button bindtap"showAll">所有图片</but…

闲不住,手写一个数据库文档生成工具

shigen坚持更新文章的博客写手&#xff0c;擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长&#xff0c;分享认知&#xff0c;留住感动。 个人IP&#xff1a;shigen 逛博客的时候&#xff0c;发现了一个很有意思的文章&#xff1a;数据库表结构导…

记录一下我hive连不上DataGrip的问题

用户名和密码都没问题&#xff0c;但报如下这个错误 原因&#xff1a;是因为我在linux上没启hiveserver2服务 解决&#xff1a; [atguiguhadoop102 hadoop]$ hiveserver2 which: no hbase in (/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/opt/module/jdk1.8…

Kotlin从0到1,让你一周快速上手!!

声明 大家好&#xff0c;这里是懒羊羊学长&#xff0c;如果需要pdf版以及其他资料&#xff0c;请加入群聊。群里每天更新面经、求职资料&#xff0c;经验分享等&#xff0c;大家感兴趣可以加一下。 Kotlin 声明1.Kotlin基础2. Kotlin函数3.Kotlin进阶4.Kotlin集合5.Kotlin高…