代码随想录算法训练营第三十七天|01背包问题、分割等和子集

01背包问题

在这里插入图片描述

题目链接:46. 携带研究材料
文档讲解:代码随想录
状态:忘了

二维dp

问题1:为啥会想到i代表第几个物品,j代表容量变化?

动态规划中,每次决策都依赖于前一个状态的结果,在背包问题中每次取物品的操作都必须考虑当前背包容量是否足够。所以使用i代表第几个物品,j代表背包容量限定。而第i个物品取和不取直接影响到最大价值总和dp[i][j]。

因此,dp[i][j]可以表示为,在容量j的条件下,取第i个物品所能得到的最大价值总和。

动态转移方程:

每次状态转移,需要考虑当前背包容量是否足够容纳物品i:

  • 如果当前物品i的重量 weight[i] 大于当前背包的容量j,则显然无法将物品i放入背包,因此 dp[i][j] 应该等于 dp[i-1][j],即不拿当前物品i时的最优解。
  • 如果当前物品i的重量 weight[i] 小于等于当前背包的容量j,则可以尝试将物品i放入背包(不能保证一定能放下)。此时,考虑两种情况:
    • 不放入物品i,也就是物品i放不下,否则能放下的话肯定是放入物品i后总价值更高!即 dp[i][j]=dp[i-1][j],和上面的情况一样!
    • 放入物品i,能放下物品i,肯定是放入后价值更高,即dp[i][j] = dp[i-1][j - weight[i]] + value[i],其中 value[i] 是物品i的价值。

考虑到上述情况,所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

二维dp题解:

    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize) {// 创建dp数组int goods = weight.length;  // 获取物品的数量int[][] dp = new int[goods][bagSize + 1];// 初始化dp数组// 创建数组后,其中默认的值就是0// 当背包的容量大于等于第一个物品的重量时,才会将取第一个物品时最大价值设为第一个物品的价值for (int j = weight[0]; j <= bagSize; j++) {dp[0][j] = value[0];}// 填充dp数组for (int i = 1; i < weight.length; i++) {for (int j = 1; j <= bagSize; j++) {if (j < weight[i]) {/*** 当前背包的容量都没有当前物品i大的时候,是不放物品i的* 那么前i-1个物品能放下的最大价值就是当前情况的最大价值*/dp[i][j] = dp[i - 1][j];} else {/*** 当前背包的容量不确定可以放下物品i* 那么此时分两种情况:*    1、放不下,所以不放物品i*    2、放物品i* 比较这两种情况下,哪种背包中物品的最大价值最大*/dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);}}}// 打印dp数组for (int i = 0; i < goods; i++) {for (int j = 0; j <= bagSize; j++) {System.out.print(dp[i][j] + "\t");}System.out.println("\n");}}//这种方法初始化时,初始化了一个(m+1)×(n+1)的二维数组,包含了额外的一行和一列,用来表示没有放入任何物品时的情况。public static void testWeightBagProblem2(int[] weight, int[] value, int bagSize) {// 创建dp数组int m = weight.length;  // 获取物品的数量int n = bagSize;int[][] dp = new int[m + 1][n + 1];  // 创建动态规划数组,行表示物品数量,列表示背包容量// 填充dp数组for (int i = 1; i <= m; i++) {  // 遍历物品for (int j = 1; j <= n; j++) {  // 遍历背包容量if (j < weight[i - 1]) {  // 如果当前背包容量小于当前物品的重量,则无法装入该物品dp[i][j] = dp[i - 1][j];  // 当前最优解等于上一个物品的最优解} else {  // 否则可以选择装入当前物品或者不装入当前物品,取两者中的最大值dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);// 不装入当前物品:dp[i - 1][j]// 装入当前物品:dp[i - 1][j - weight[i - 1]] + value[i - 1]// value[i - 1] 表示当前物品的价值}}}// 打印dp数组for (int i = 0; i <= m; i++) {for (int j = 0; j <= bagSize; j++) {System.out.print(dp[i][j] + "\t");  // 打印每个位置的最优值}System.out.println("\n");  // 换行}}

优化:

可以考虑用另一方式来定义dp[i][j]的含义,即dp[i][j] 表示考虑前i个物品,在背包容量为j时可以达到的最大价值。

那么dp[i][j]就可能从两种状态转换而来。

  • 第一种是当前物品i放不下,那么dp[i][j]=dp[i−1][j],也就是继承上一个状态的最优解
  • 第二种是可以放当前物品i,那么dp[i][j]=dp[i−1][j−weight[i−1]]+value[i],也就是在上一个状态的继承上加上当前物品i的价值

因此,可以的到递推公式:dp[i][j] = max(dp[i][j], dp[i - 1][j - weight[i - 1]] + value[i]);

优化后题解:

// 使用了优化后的递推公式 dp[i][j] = max(dp[i][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
public static void testWeightBagProblem3(int[] weight, int[] value, int bagSize) {int length = weight.length;// 创建二维数组dp,dp[i][j]表示考虑前i个物品,在背包容量为j时的最大价值int[][] dp = new int[length + 1][bagSize + 1];// 遍历每个物品for (int i = 1; i <= length; i++) {// 遍历每个背包容量for (int j = 1; j <= bagSize; j++) {// 先假设不选第i个物品,继承上一个状态的最优解dp[i][j] = dp[i - 1][j];// 判断如果当前背包容量能够容纳第i个物品if (weight[i - 1] <= j) {// 考虑选择第i个物品后的最优解,这里要注意value[i - 1])是第i个物品的价值,因为第一行和第一列用0填充了,但是value数组是从索引0开始有意义的。dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);}}}
}

一维dp:

在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);也就是上面优化后的代码。
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。此时,dp[j]的状态要么是上次取完物品i-1的状态,要么是加入物品i的状态。

 在取物品0的时候,dp[j]会进行第一轮更新[0  15  15  15	 15]在取物品1的时候,dp[j]会进行第二轮更新[0  15	 15	 20	 35]在取物品2的时候,dp[j]会进行第三轮更新[0  15	 15  20	 35]

所以递推公式为,dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

  • 左边的 dp[j]:表示在更新当前容量 j 时,新的 dp[j] 值。

  • 右边的 dp[j]:表示背包装不下物品i,所以继承上次容量为 j 时取完物品i-1的最大价值,即在未考虑物品 i 的情况下的最大价值。

  • 右边的 dp[j - weight[i-1]] + value[i]:表示在当前更新之前,容量为 j - weight[i-1] 时的最大价值,加上当前物品 i 的价值。

为什么需要从后向前遍历?

在使用一维数组 dp 时,从后向前遍历容量 j 是为了避免在同一轮次中使用已经更新的值。这保证了每个物品 i 在更新时只被计算一次,不会重复使用。

举个例子:
物品 1: 重量 2,价值 3
物品 2: 重量 3,价值 4
背包容量为 5。

从前向后遍历:
我们从前向后遍历容量 j 来更新 dp 数组。看看会发生什么情况。

遍历第一个物品(重量 2,价值 3):
j = 2:
dp[2] = max(dp[2], dp[2 - 2] + 3) = max(0, 0 + 3) = 3
更新后 dp = [0, 0, 3, 0, 0, 0]

j = 3:
dp[3] = max(dp[3], dp[3 - 2] + 3) = max(0, 0 + 3) = 3
更新后 dp = [0, 0, 3, 3, 0, 0]

j = 4:
dp[4] = max(dp[4], dp[4 - 2] + 3) = max(0, 3 + 3) = 6
更新后 dp = [0, 0, 3, 3, 6, 0]
从这里开始就出现问题了,求dp[4]的时候使用了更新后的dp[2]的值。

j = 5:
dp[5] = max(dp[5], dp[5 - 2] + 3) = max(0, 3 + 3) = 6
更新后 dp = [0, 0, 3, 3, 6, 6],求dp[5]的时候使用了更新后的dp[3]的值。

遍历第二个物品。。。。(略)

从后向前遍历:
遍历第一个物品(重量 2,价值 3):
j = 5:
dp[5] = max(dp[5], dp[5 - 2] + 3) = max(0, 0 + 3) = 3
更新后 dp = [0, 0, 0, 0, 0, 3],这里使用的dp[2]是还没更新的值。
…(略)

一维dp代码:

    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize) {int[] dp = new int[bagSize + 1];for (int i = 0; i < weight.length; i++) {for (int j = bagSize; j >= weight[i]; j--) {//因为i在更新,所以max中的dp[j]都是上一层中的dp[j],所以隐式地实现了"dp[i - 1]一层拷贝到dp[i]"dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);//max中的dp[j]是取上一个物品时对应容量j的最大价值}}for (int j = 0; j <= bagSize; j++) {System.out.print(dp[j] + "\t");}}

416. 分割等和子集

在这里插入图片描述

题目链接:416. 分割等和子集
文档讲解:代码随想录
状态:感觉像碰运气做出来的。。

思路:

第一步读题:分割成两个子集,使得两个子集的元素和相等,那么可以考虑先求和再除以2,得到目标值。对于nums中的数字,尝试不同的取值求和,只要得到和为target说明一定可以分成两个和相等的子集。所以可以考虑使用背包解题。

第二步判断背包类型:

  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入。

以上分析完,我们就可以套用01背包,来解决这个问题了。

第三步:动规五部曲分析。

  • dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。在本题中就是取不同的值求得最大和dp[j]。本题中如果dp[j]=j就是满足条件了。
  • 01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
  • 初始化,取第一个数字前,dp[j]都为0
  • 确定遍历顺序:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
  • 举例推导dp数组:如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j

题解:

// 一维dp实现
public boolean canPartition(int[] nums) {int sum = 0;// 计算数组总和for (int num : nums) {sum += num;}// 如果总和是奇数,不可能分成两个相等的子集if (sum % 2 == 1) {return false;}// 目标值是总和的一半int target = sum / 2;// 创建一维dp数组,dp[j]表示是否存在子集和为jint[] dp = new int[target + 1];// 遍历所有数字for (int i = 0; i < nums.length; i++) {// 倒序遍历所有可能的和for (int j = target; j > 0; j--) {// 如果当前数字小于等于目标和,更新dp数组if (nums[i] <= j) { // 刚开始没注意到这里, 其实最好写在for循环的判断条件中, 因为使用的数字肯定不能大于目标和dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);}}// 剪枝,只要有满足条件即可提前退出if (dp[target] == target) {return true;}}// 检查是否可以找到和为target的子集return dp[target] == target;
}

二维dp题解:

// 二维dp实现
public boolean canPartition(int[] nums) {int sum = 0;// 计算数组总和for (int num : nums) {sum += num;}// 如果总和是奇数,不可能分成两个相等的子集if (sum % 2 == 1) {return false;}// 目标值是总和的一半int target = sum / 2;// 创建二维dp数组,dp[i][j]表示前i个数能否组成和为jint[][] dp = new int[nums.length + 1][target + 1];// 遍历所有数字for (int i = 1; i <= nums.length; i++) {// 遍历所有可能的和for (int j = 1; j <= target; j++) {if (nums[i - 1] > j) {// 如果当前数字大于目标和,不能选当前数字,继承上一个状态的结果dp[i][j] = dp[i - 1][j];} else {// 否则,可以选择或者不选择当前数字,取两者的最大值dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i - 1]] + nums[i - 1]);}}}// 检查是否可以找到和为target的子集return dp[nums.length][target] == target;
}

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

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

相关文章

Java中使用键盘录用【Scanner】遇到的问题

目录 一、空格截断问题&#xff1a;二、next()、nextInt()、nextDouble()等nextXxx()与nextLine()连用、混用的问题&#xff1a;问题描述&#xff1a;代码演示问题问题原因&#xff1a;解决办法&#xff1a;示例代码&#xff1a; 最后 Java中使用键盘录入&#xff0c;尤其是通过…

ROS2开发机器人移动

.创建功能包和节点 这里我们设计两个节点 example_interfaces_robot_01&#xff0c;机器人节点&#xff0c;对外提供控制机器人移动服务并发布机器人的状态。 example_interfaces_control_01&#xff0c;控制节点&#xff0c;发送机器人移动请求&#xff0c;订阅机器人状态话题…

力扣SQL50 员工的直属部门 子查询 双重

Problem: 1789. 员工的直属部门 &#x1f468;‍&#x1f3eb; 参考题解 Code select employee_id, department_id from Employee where primary_flag Y # Y 表明是直属部门 or employee_id in (select employee_idfrom Employeegroup by employee_idhaving count(employee…

【STM32-MAP文件分析】

STM32-MAP文件分析 ■ MDK编译生成文件简介■ .o■ .axf■ .hex■ .crf■ .d■ .dep■ .lnp■ .lst■ .map■ .build_log.htm■ .htm 文件■ .map 文件 ■ map文件分析■ map 文件的 MDK 设置■ 1. 要生成 map 文件 在 Listing 选项卡里面■ 2. Keil5 中打开.map 文件 ■ map 文…

信息学奥赛初赛天天练-38-CSP-J2021阅读程序-约数个数、约数和、埃氏筛法、欧拉筛法筛素数应用

PDF文档公众号回复关键字:20240628 2021 CSP-J 阅读程序3 1阅读程序(判断题1.5分 选择题3分 共计40分 ) 01 #include<stdio.h> 02 using namespace std; 03 04 #define n 100000 05 #define N n1 06 07 int m; 08 int a[N],b[N],c[N],d[N]; 09 int f[N],g[N]; 10 11 …

矩阵快速幂

矩阵快速幂 矩阵&#xff1a; 一个矩阵 A A A&#xff0c;是由 n m n\times m nm 个数字组成&#xff0c; B B B 由 m p m\times p mp 组成&#xff0c;详见下。 A [ a 1 , 1 , a 1 , 2 , a 1 , 3 ⋯ a 1 , m a 2 , 1 , a 2 , 2 , a 2 , 3 ⋯ a 2 , m ⋅ ⋅ ⋅ a n , …

javaSE知识点整理总结(上)

目录 一、面向对象 1. 类、对象、方法 2.面向对象三大特征 &#xff08;1&#xff09;封装 &#xff08;2&#xff09;继承 &#xff08;3&#xff09;多态 二、常用类 1.Object类 2.Array类 3.基本数据类型包装类 4.String类 5.StringBuffer类 6.Math类 7.Random…

摄影楼电子相册打开的正确方式,快来看看

​随着科技的不断发展&#xff0c;电子相册已经成为许多人存储和分享照片的重要方式。然而&#xff0c;你知道如何正确打开电子相册吗&#xff1f;今天&#xff0c;我就来教大家一下电子相册的正确打开方式&#xff0c;快来学习一下吧&#xff01; 第一步&#xff1a;选择合适的…

QT拖放事件之七:子类化QMimeData,实现对多个自定义类型进行数据

1、前提说明 /*自定义的MIME类型数据存储在QMimeData对象中, 存在两种方法:1. setData(...)可以把自定义类型的数据以QByteArray的形式直接存储在QMimeData中,但是使用此方法一次只能对一个MIME类型进行处理(可参考 QT拖放事件六:自定义MIME类型的存储及读取demo ) 一文。…

成立近30年,它如何找到政企采购突破点?

回看中国采购行业的发展&#xff0c;大致可以被分为四个阶段&#xff1a;上世纪90年代的传统采购时代、本世纪初的ERP采购时代、近10年的SRM采购时代以及2018年以来开启的数字化采购时代。近年来&#xff0c;大数据、人工智能和物联网的高速发展&#xff0c;为采购信息化提供底…

从探头到传感器:德思特数字化仪的全面结合与应用

电压探头可以用于转换信号电平、改变阻抗或提供更方便的连接方法。而包括电流探头、加速度计和光电倍增管在内的传感器或变送器&#xff0c;则可以将各种物理量转换为电信号。这两种输入设备都受到德思特数字化仪的支持。这篇应用笔记将介绍如何将德思特板卡式数字化仪和探头、…

一文2000字记录基于jmeter+perfmon的稳定性测试

01、任务情况 1、任务总览 本次平台稳定性测试的目的在于&#xff1a;在服务器压力处于较饱和&#xff08;达到80%系统最大TPS&#xff09;压力之下&#xff0c;在较长时间&#xff08;>8小时&#xff09;之内观测服务器稳定性问题&#xff0c;以及资源使用情况和异常。 …

springcloud-gateway 路由加载流程

问题 Spring Cloud Gateway版本是2.2.9.RELEASE&#xff0c;原本项目中依赖服务自动发现来自动配置路由到微服务的&#xff0c;但是发现将spring.cloud.gateway.discovery.locator.enabledfalse 启动之后Gateway依然会将所有微服务自动注册到路由中&#xff0c;百思不得其解&a…

【仿真建模-anylogic】开发规范

Author&#xff1a;赵志乾 Date&#xff1a;2024-06-28 Declaration&#xff1a;All Right Reserved&#xff01;&#xff01;&#xff01; 0. 说明 实际模型开发过程中&#xff0c;对遇到的问题进行总结归纳出以下开发规范&#xff0c;仅供参考&#xff01; 1. 强制性规范 1…

pcr实验室和P2实验室装修设计中的区别

PCR实验室和P2实验室在装修设计的区别是什么&#xff1f;PCR实验室指的是基因扩增实验室&#xff0c;而P2实验室是指生物安全实验室中的一个分类&#xff0c;是生物安全防护达到二级的实验室。那么PCR实验室和P2实验室装修设计标准是什么&#xff1f;实验室装修公司小编为您详解…

数据分析三剑客-Matplotlib

数据分析三剑客 数据分析三剑客通常指的是在Python数据分析领域中&#xff0c;三个非常重要的工具和库&#xff1a;Pandas、NumPy和Matplotlib。Pandas主要负责数据处理和分析&#xff0c;NumPy专注于数值计算和数学运算&#xff0c;而Matplotlib则负责数据可视化。这三个库相…

动手学深度学习(Pytorch版)代码实践 -计算机视觉-44目标检测算法综述:R-CNN、SSD和YOLO

41~44目标检测算法综述&#xff1a;R-CNN、SSD和YOLO 1. 区域卷积神经网络 (R-CNN 系列) 1.1 R-CNN 使用启发式搜索算法来选择锚框。使用预训练模型对每个锚框提取特征&#xff08;每个锚框视为一张图片&#xff0c;使用 CNN 提取特征&#xff09;。训练 SVM 进行类别分类&a…

计算机体系结构 量化研究方法

在第一章中看到关于微处理器中dynamic energy 和 dynamic power的定义觉得有些奇怪&#xff0c;特别记录一下。 上面的定义是取决于上下文的&#xff1a;动态能量可以理解为在一个时钟周期内&#xff0c;由电容充放电消耗的能量总和&#xff0c;而动态功率则是这种能量消耗在单…

Vite脚手架+Vant组件库初始化前端项目

脚手架概念&#xff1a; 在前端开发中&#xff0c;脚手架&#xff08;Scaffold&#xff09;是指一个用于快速搭建项目基础结构的工具或模板。脚手架包含了项目所需的基本文件结构、配置文件、依赖管理等内容&#xff0c;使开发者能够更快速地开始项目开发&#xff0c;而不必从…

常微分方程算法之编程示例四(龙格-库塔法)

目录 一、算例一 1.1 研究问题 1.2 C++代码 1.3 计算结果 二、算例二 2.1 研究问题 2.2 C++代码 2.3 计算结果 一、算例一 本节我们采用龙格-库塔法(Runge-Kutta法)求解算例。 龙格-库塔法的原理及推导请参考: 常微分方程算法之龙格-库塔法(Runge-Kutta法)…