【算法练习Day35】01背包问题分割等和子集

news2024/11/26 11:36:46

在这里插入图片描述

​📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:练题
🎯长路漫漫浩浩,万事皆有期待

文章目录

  • 01背包问题
  • 分割等和子集
  • 总结:

这一期我们来介绍背包问题,在leetcode中没有纯粹的01背包问题的具体题目,但是有一些可以用到01背包问题思路的题目。

许多的背包问题都是由01背包演化而来的,所以01背包的重要性,应该是不言而喻的

01背包问题

下面说一下01背包具体的题目要求是什么:给定一些物品,物品都具有两个单独的属性,物品重量和物品价值,且每一个物品都只有一个,现给定一个固定容量的背包,要求是如何选取物品填充背包,可以使得背包内物品价值最高?

是这样的一道题,区分背包问题的类型通常是和物品有关,这里要求物品只有一个,这也是01背包的特性所在。

两种解决方案,都是动态规划的,只不过第一种是二维数组实现,第二种是一位数组实现。那么我们为什么不先讲一维数组实现呢?因为二维数组实现是基础,只有搞懂二维数组,才有利于我们取理解一维数组的实现。

仍然根据之前讲的动规五部曲来实现

确定dp数组的含义:dp数组我们之前说了是一个二维数组,那么i和j代表了什么呢?i代表从物品0-i中任取物品,j代表了背包的容量。dp【i】【j】代表了当前取到这个物品时候,背包价值是多少。这是对于该二维数组的一个较为抽象的描述,我们可以采用纸上画出二维数组的方法来帮助理解,就是画一个棋盘格子,行上我们用背包容量去表示,比如从背包容量0到容量j,而列上我们用物品0-i来表示。画出了格子相信大家就可以很好的理解了。

递推公式:我们根据对dp数组的定义可得出,当前背包可以有装入当前物品i和不装入当前物品i两种状态!不装物品i可以表示为dp【i-1】【j】,因为不装物品i和它的上一行同一列对应的价值是相等的,这里如果看不懂一定要回头看一遍dp的定义以及自己画出来的表格。这里你也可以理解为遍历上一层的时候是装物品1,这一层是装了物品1但是没装物品2,这样理解就很好想了。当然你可以理解为,当前背包容量可能不足,装不了物品i。那么装载物品i的情况是什么样的呢?先给出结论装载物品i是dp【i-1】【j-weight【i】】+value【i】。为什么是这样的呢?我们对于dp【i-1】【j-weight【i】】+value【i】的解释是这样的:dp【i-1】是未放入物品i,而【j-weight【i】】相当于为了放入物品i我们要找到一个合适的地方,这就相当于回溯一样,是确定这个背包是否还能装的进去物品i,如果装不进去把多于重量减去,而+value【i】就是计算价格的。

根据这两个数,我们可以推出递推公式

即dp【i】【j】=max(dp【i-1】【j】,dp【i-1】【j-weight【i】】+value【i】)

这里还有要搞清楚的点,为什么要取它们的最大值呢?

不是放入物品就一定是价值最大吗?并不是,放入物品你要有空余的背包容量,我们这里判断的是加入i和不加入i的价格,那么放入i如果本来就有地方放,那肯定是最大价值,那如果没有地方放,需要先扔出一些物品呢?那就不一定谁更大了!

dp数组的初始化:dp数组的初始化也是有讲究的。根据递推公式的推算,以及画图的理解,我们知道我们要求的dp【i】【j】是由它的上一个数和它的左上方的数推理得到的。最左侧一列也就是背包容量为0的那一列,对应的全部物品都无法放入,全都初始化为0,而最上面一行是针对物品0在各个背包容量上能否放入,我们把能够放入的初始化为物品0的价值。其他的位置都可以初始化为0,因为我们的递推公式的缘故,并没有比较有关于它本身的数值,所以其实初始化为什么数字都可以。

遍历顺序:遍历顺序同样有讲究。虽说根据递推公式,基本的遍历顺序应当是从左到右,从上到下。但是我们是遍历二维数组,那么需要两个for循环,是先遍历背包容量,还是先遍历各个物品呢?实际上两种遍历都可以。如果是先遍历物品再遍历背包,那就是以行从左到右依次填满,当求i,j位置时候,它的上面和左上方一定是先被填充的,所以可以出答案。事实上通常采用这种方法,因为便于逻辑上的理解。但是先遍历背包再遍历物品呢?那就是背包先不动,然后走到第二次循环遍历物品,也就是以列从上到下填充,但是当我们求i,j时候,发现它的上方和左上方也是一定有值的,所以说,这两种遍历方式理论上都是可行的!

数组打印:这就没什么好说的了,就是当想不明白,或者出现什么结果上问题了,可以把整个数组打印出来看一看,是哪里出了问题。

下面给出一个可以运行的代码,大家可以跑一下自行体会

void test_2_wei_bag_problem1() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagweight = 4;
 
    // 二维数组
    vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
 
    // 初始化
    for (int j = weight[0]; j <= bagweight; j++) {
        dp[0][j] = value[0];
    }
 
    // weight数组的大小 就是物品个数
    for(int i = 1; i < weight.size(); i++) { // 遍历物品
        for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
 
        }
    }
 
    cout << dp[weight.size() - 1][bagweight] << endl;
}
 
int main() {
    test_2_wei_bag_problem1();
}

初始化的代码就是先看哪个容量首先能装得下物品0,就放入那个地方然后向后遍历,由于我们的dp数组定义j是背包容量所以会这么写。下一个需要注意的地方是,计算i,j值的时候,当我们判断如果本来总空间都不够放入该物品那么肯定就放不下了,直接让它等于上一个放物品时候的价值。当发现当前要放入的物品,是比总空间小的话,那么可能扔出一些物品还是能放进去的,这个时候我们再比较。

接下来再来介绍,一维数组对于01背包问题的求解,一维数组的解决思路,和二维数组的解决整体思路是一样的,但是要完全看懂二维数组是如何解决的,大概率才能弄的懂一维数组。

一维数组是对二维数组的压缩,将二维数组压缩成一维,然后每次更新一维数组的全部值,以实现对二维数组的每一行的滚动,所以也叫作滚动数组的方法。

dp【j】数组的含义:dp数组的含义是代表了当前容量下所能存储的最大价值。这个数组我们上面也说了,需要反复遍历之后,才能得到最大值,所以它通常都不是一次就确定下来,当前最大值是多少的。我们为什么将里面的变量取为j呢,是为了和上面的二维数组做对比,我们保留下来的实际是二维数组的背包容量这一块,而非物品状态。

递推公式:这里我会好好解释一下为什么递推公式是这样,它是怎么推出来的?先写结论递推公式为:dp【j】=max(dp【j】,dp【j-weight【i】+value【i】)

我们把它压缩为一维数组不代表,物品就不遍历了,物品一定还是要遍历的,不然你如何知道我们放了什么东西进去呢??价格又是多少呢?压缩到一维数组后,我们这个数组每次利用的是,上一次放入东西后的总价格,和当前我们如果放入物品i得到的价格进行对比,我们比较的上一次的dp【j】这也就代表了二维数组中的上一层,一维数组将它保留到本层,然后比较之后再将当前格子价格给覆盖掉。完成了更新滚动数组,这里希望大家能够仔细地体会一下一维数组是如何巧妙地得到二维数组的上一行!正是我们上一次遍历填数后的一维数组!解释清楚了这一点,那么后面的+value【i】,【j-weight【i】】也不难理解了,这些都是讲二维数组讲过的东西,不再赘述。

dp数组初始化:初始化dp【0】是一定为0的,因为无论是哪一层,0背包容量肯定装不了东西,价值就自然为0,那其他的位置呢?实际上也初始化为0,这是由于我们的物品价格一定是正数,且一维数组需要比较当前dp【j】的位置,如果你初始化给的数过大,它影响到的是你第一次填数,过大会导致第一次填数填不进去,进而影响了后面的价格覆写。虽然你只看递推公式,感觉的是填数比较的是上一层的数,和初始化无关,但是实际上你没有注意到,初始化数字会影响到你第一层的价格填写,应当引起重视,当过了第一层价格填写后,初始化便不对之后的滚动数组产生影响了。

遍历顺序:遍历顺序,还是从左往右从上到下的遍历吗?还是先遍历物品还是背包容量都是可以的吗?这里就相当的有讲究了!遍历顺序一定是也只能是先遍历物品,再遍历背包,且背包需要从后往左遍历。因为什么呢?这是由于滚动数组的特性,是要根据上一层填写的数值来更新本层循环的数值,但是如果我们要从左到右遍历背包的话,第一个数据刚更新完,我们更新第二个数据此时背包如果有空,它会更新为第二个max比较的部分,这样会使第二个数据过早的应用第一个数据,也就是说,本来我们更新第二个数据时候,用到的可能是第一个数据,但是这个数据必须是上一层的,也就是旧的数据,但是你正向遍历背包,第二个数据应用上的就是刚刚更新完的数据,也就是本层的数据,这里好好体会一下。我们可以自己模拟一下,如果是正向遍历背包,我们在第一次的填数时候,假设物品的重量为:1,3,4物品价值为:15,20,30。最大背包容量为4。如果按照这种情况,那么第一次更新背包为1时候,放入物品0,价值为15,背包容量为2时候,根据递推公式可得:dp【2-weight【0】】+value【0】得到背包容量为2时候,价值为30。实际上不可能得到这么多,这相当于把物品0连续放入两次,但是每个物品只有一个,这显然不符题意。所以正确的做法是从后向前遍历背包。

那为什么两层循环,遍历的东西,不能够像二维数组那样可以颠倒顺序呢?

还是和滚动数组特性有关,二维数组可以颠倒顺序是因为我们并不需要担心当前层的数据会覆盖上一层的数据,因为二维数组的各个层次之间是彼此分隔开的,而一维数组不一样,我们本来先遍历物品是使物品不动,通过改变背包容量来更新价值,然后再看第二个物品,第三个物品,是否还可以加入到背包内,使得当前背包价值更高。如果是先遍历背包呢?那么背包容量不变,将物品依次放入,更新价值。乍一看好像思路也没有毛病,但是实际上地推公式是要在装入第二个物品的时候,要比较上一个旧数据,但是我们在当层循环是固定背包容量的,我怎么能改变其他背包的数据呢?这一点在第一次填数时候体现的十分明显,我们背包放第二个物品需要找到,没放第二个物品的起始条件,但实际上那个空我们还没有加入东西呢!!就已经连续更新这个容量的背包了!它和第一种遍历方法完全不一样,第一种是将各种背包容量先更新一次,然后放第二个物品再更新一次全部背包容量,而这种遍历方法是将一个背包容量固定死,一直更新,一直放东西,然后才能去更新其他容量。刚看到这的朋友可能不是十分理解,说的有些抽象,最好的解决办法,就是将代码放到你的编译器里调试一遍,你就能深刻的体会到,究竟有何不同了!

给出一维数组实现的代码:

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;
}
 
int main() {
    test_1_wei_bag_problem();
}

信息量很大,希望大家看完分析可以有所收获。

下面我们来看一道根据具体的题目应用01背包的实例。

分割等和子集

416. 分割等和子集 - 力扣(LeetCode)

这道题实际上想到用01背包来解的思路,我觉得是有点难度的,并不是实现时候的思路有多难,而是想到用这种思路来解答,是有一些难度的。

分析题目,我们要将一个数组分成两部分,使两部分数组的数据和相等。那么最好是要想到我们可以使一个数组数据和等于整个数组数据和的一半,要想到这一点,才能好解题目。

这里数组的数据就同时代表了01背包里物品的重量和价格,想出这一点是用上背包模板的关键。我们可以用dp数组来模拟,然后用数据来同时表示重量和价格,最后我们来看dp【j】==j这个时候就说该数组可以被分割。为什么?因为重量和价值相等,dp数组表示的是当前重量的最大价值,这里相等的话,也就是说的dp【j】==j了,重量和价值是一样的。

dp数组的含义和递推公式以及遍历顺序完全和01背包一样。初始化也是全都变成0。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        vector<int>dp(10001,0);int sum=0;
        for(auto i:nums)sum+=i;
        if(sum%2==1)return false;
        int target=sum/2;
        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;
        else return false;
    }
};

当我们发现,给定数组和无法准确的分成一半,也就是说有余数的话,那么说明怎么分也不可能将数组切成数组和相等的两部分了。可以直接return了。

总结:

今天我们完成了01背包问题、分割等和子集两道题,相关的思想需要多复习回顾。接下来,我们继续进行算法练习。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述

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

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

相关文章

C语言从入门到精通之【编译过程】

&#xff01;&#xff01;&#xff01;本文内容毕业生面试必问哈。 编译过程 编译包含四个阶段&#xff0c;预处理&#xff08;Preprocessing&#xff09;、编译&#xff08;Compilation&#xff09;、汇编&#xff08;Assembly&#xff09;、链接&#xff08;Linking&#x…

JAVA 实现PDF转图片(spire.pdf.free版)

1.引入jar包 导入方法1&#xff1a; 手动引入。将Free Spire.PDF for Java下载到本地&#xff0c;解压&#xff0c;找到lib文件夹下的Spire.PDF.jar文件。在IDEA中打开如下界面&#xff0c;将本地路径中的jar文件引入Java程序&#xff1a; 导入方法2&#xff1a;如果您想通过…

Day17力扣打卡

打卡记录 参加会议的最多员工数&#xff08;拓扑排序 分类讨论&#xff09; 链接 计算内向基环树的最大基环&#xff0c;基环树基环为2的情况分类讨论。 class Solution { public:int maximumInvitations(vector<int> &favorite) {int n favorite.size();vector…

STM32中微秒延时的实现方式

STM32中微秒延时的实现方式 0.前言一、裸机实现方式二、FreeRTOS实现方式三、定时器实现&#xff08;通用&#xff09;4、总结 0.前言 最近在STM32驱动移植过程中需要用到微秒延时来实现一些外设的时序&#xff0c;由于网上找到的驱动方法良莠不齐&#xff0c;笔者在实现时序过…

链表指定节点的插入

向现有链表中插入结点&#xff0c;根据插入位置的不同&#xff0c;可分为以下 3 种情况&#xff1a; 插入到链表的头部&#xff0c;作为新的链表中第一个存有数据的结点&#xff08;又称为”首元结点”&#xff09;&#xff1b;插入到链表中某两个结点之间的位置&#xff1b;插…

通达信对子数的指标公式大全

对子数&#xff1a;形如9.22,12.21, 4.44,11.00等 顺子数&#xff1a;形如&#xff13;.89,4.98,7.56等 进一步的了解,自行百度。 在通达里查找价格为对子数的个股&#xff0c;有两个自带的函数&#xff0c; INTPART(X),返回数值的整数部分&#xff0c; FRACPART&#xff…

安达发|APS生产排程解决五金制造企业的需求

在五金制造行业中&#xff0c;生产排程一直是一个非常重要的环节。然而&#xff0c;由于五金行业的特点和痛点&#xff0c;传统的生产排程方法往往难以满足企业的需求。本文将针对五金行业的痛点&#xff0c;探讨如何利用APS生产排程解决这些问题。 首先&#xff0c;我们需要了…

代码随想录二刷Day 56

1143.最长公共子序列 本题和动态规划&#xff1a;718. 最长重复子数组 (opens new window)区别在于这里不要求是连续的了&#xff0c;但要有相对顺序&#xff0c;即&#xff1a;"ace" 是 "abcde" 的子序列&#xff0c;但 "aec" 不是 "abcd…

大模型时代目标检测任务会走向何方?

参考&#xff1a; 大模型时代目标检测任务会走向何方&#xff1f; 细数从常见的目标检测到现在 MLLM 盛行的时代&#xff0c;和 Object Detection 的任务以及近期涌现的新任务。>>加入极市CV技术交流群&#xff0c;走在计算机视觉的最前沿 你或许很好奇&#xff0c;现在…

人工智能时代八大类算法你了解吗?(文末包邮送书6本)

文章目录 本文导读1. 关联规则分析2. 回归分析3. 分类分析4. 聚类分析5. 集成学习6. 自然语言处理7. 图像处理8. 深度学习9. 书籍推荐&#xff08;包邮送书6本&#xff09; 本文导读 从零带你了解人工智能时代需要掌握的8大类算法&#xff0c;包括基础理论、关联规则分析、回归…

Vue 创建自定义 ref 函数

Vue 创建自定义 ref 函数 customRef customRef 用于&#xff1a;创建一个自定义的 ref 函数&#xff0c;并对其依赖项跟踪和更新触发进行显式控制。 使用 customRef 创建自定义 ref 函数 // 创建自定义 ref 函数 function myRef(value) {return customRef((track, trigger) &g…

自动曝光算法(第一讲)

序言 失业在家无事&#xff0c;想到以后换方向不做自动曝光了&#xff0c;但是自动曝光的工作经验也不能浪费了&#xff0c;准备写一个自动曝光的教学&#xff0c;留给想做自动曝光的小伙伴参考。笔者当时开发自动曝光没有按摄影的avtvevbvsv公式弄&#xff0c;而是按正确的增…

[架构之路-251/创业之路-82]:目标系统 - 纵向分层 - 企业信息化的呈现形态:常见企业信息化软件系统 - 商业智能、决策支持系统、知识管理

目录 前言&#xff1a; 一、企业信息化的结果&#xff1a;常见企业信息化软件 1.1 商业智能 - 管理层 1.1.1 什么是商业智能What 1.1.1.1 商业智能常见工具 1.1.2 为什么需要商业智能Why&#xff1f; 1.1.3 谁需要商业智能who&#xff1f; 1.1.4 商业智能在企业管理中的…

微信小程序开发(搭建)

首先去微信开发者网站下载微信开发者工具 然后打开电脑命令框wincmd 全局安装 vue-clinpm install --global vue-cli创建一个基于 mpvue-quickstart 模板的新项目vue init mpvue/mpvue-quickstart my-project安装依赖cd my-projectnpm install启动构建npm run dev 记得为vue配…

Docker dnmp 多版本php安装 php8.2

Laravel9 开发需要用到php8.1以上的版本&#xff0c;而dnmp只支持到php8.0。安装php8.2的步骤如下&#xff1a; 1. 从/services/php80目录复制一份出来&#xff0c;重命名为php82&#xff0c;extensions目录只保留 install.sh 和 install-php-extensions 这两个文件 2. 修改.en…

C++使用栈实现简易计算器(支持括号)

使用C实现&#xff0c;使用系统自带stac 支持括号处理支持小数计算支持表达式有效性检查支持多轮输入。 运行结果示例&#xff1a; 代码&#xff1a; #include <iostream> #include <stack> #include <string> using namespace std;//判断是否是数字字符 …

手动仿射变换

开发环境&#xff1a; Windows 11 家庭中文版Microsoft Visual Studio Community 2019VTK-9.3.0.rc0vtk-example参考代码目的&#xff1a;学习与总结 demo解决问题&#xff1a;通过仿射控件vtkAffineWidget对目标actor进行手动的拖拽的仿射变换 关键类&#xff1a;vtkAffineWi…

PostGIS轨迹分析——简化轨迹

需求 对轨迹线进行简化,并将原始轨迹上的两个特征点拉取到简化后的轨迹上 简化线 红色线是简化后的轨迹线,蓝色线是原始轨迹,有两个特征点 知识点: st_makeline函数将点连成线st_simplify简化线函数,其中第二个参数为坐标系的单位,0.002度大概代表0.002x1.11x10^5≈22…

使用 ElementUI 组件构建 Window 桌面应用探索与实践(WinForm)

零、实现原理与应用案例设计 1、原理 基础实例 Demo 可以参照以下这篇博文&#xff0c; 基于.Net CEF 实现 Vue 等前端技术栈构建 Windows 窗体应用-CSDN博客文章浏览阅读291次。基于 .Net CEF 库&#xff0c;能够使用 Vue 等前端技术栈构建 Windows 窗体应用https://blog.c…

通过xshell传输文件到服务器

一、user is not in the sudoers file. This incident will be reported. 参考链接&#xff1a; [已解决]user is not in the sudoers file. This incident will be reported.(简单不容易出错的方式)-CSDN博客 简单解释下就是&#xff1a; 0、你的root需要设置好密码 sudo …