【算法篇-动态规划】手撕各大背包问题 —— 01背包

news2024/11/29 6:27:35

背包问题

  • 1. 最基础的背包 —— 01背包 (必看)
    • 1.1 分析
    • 1.2 状态转移方程 和 边界条件
    • 1.3 代码
    • 1.3.1 代码模拟
  • 1.4 空间复杂度的优化
    • 1.4.1 错误的优化方式
    • 1.4.2 正确的优化方式
  • 1.5 终极版优化
  • 总结

本文章参考自 B站 董晓算法

董晓算法

1. 最基础的背包 —— 01背包 (必看)

在这里插入图片描述

所谓的01背包问题就是每个物品放么放 0 次要么只能放 1 次,这就是01背包问题。
题目要求很简单,就是给你 几个物品,这几个物品有各自的重量(或体积)和价值,要求你在有限的背包内放下最大价值的物品选择。 看样例输入,就是 下面会给你 3 个物品 ,然后背包 总重量为 6,下面的 3 个物品 第一项是 重量 ,第二项是 价值。 可以得到当 选择
2 3 和 4 6 这个组合时,可以得到最大价值 9 。

动态规划分析法
1.确定状态变量(函数)
2.确定状态转移方程
3.确定边界条件

本题的 最大价值函数为 f(i,j),i 为物品的数量j 为背包的容量
那么 设 函数 f [ i ] [ j ],表示前 i 件物品放入容量为 j 的背包的最大价值
那么最终的最大价值就是物品数量 i 从 0 增长到 n ,背包容量 j 从 0 增长到 m 时的 f [ n ][ m ] 值

注意这里的 “前 i 件物品”,它是状态转移的关键,即从上一个状态转移到下一个状态我们会如何做选择。
它也是我们保证 最终 f [ n ][ m ] 是最大价值的关键 !

1.1 分析

已知我们的状态变量函数是
f[i][j]
表示前 i 件物品放入容量为 j 的背包的最大价值

当背包容量为 j ,我们要考虑 第 i 件物品 能否放入 ? 是否放入 ?

以下w[i] 表示第i件物品的重量,v[i] 表示第 i 件物品的价值
1.如果当前背包的容量 j < w[ i ],则 f[ i ][ j ] = f[ i - 1][ j ]

解释 : 因为 第 i 件物品的重量大于当前背包的容量 j ,所以我们不放入它。
状态转移后 前 i 件物品 的价值和 它相同。

2.如果当前背包的容量 j >= w[i] ,则能放入它,但是要比较代价
(1) 如果第 i 件物品不放入背包,则还是 f[ i ][ j ] = f[ i - 1][ j ]
(2)如果第 i 件物品放入背包,则 f[ i ][ j ] = f[i - 1][ j - w[ i ]] + v[ i ];
在放与不放中选择最优的选择,是我们要考虑的。

解释:(1) 中,因为我们不放入背包,所以状态转移后,和前 i 件的状态相同。
(2)中,如果 第 i 件物品 放入背包 ,则背包容量还剩 j - w[ i ] , 所以要取前 i - 1件物品放入背包剩余容量 j - w[i]所获得的最大价值
f[ i - 1 ][ j - w[ i ] ]

我们这时的目的就是在放与不放这两种选择中,选择最优的选择

看个例子
在这里插入图片描述

1.2 状态转移方程 和 边界条件

经过上面的分析,我们可以得到这样一个图
在这里插入图片描述
一种是当前状态和上一状态相同,放在表格中,就是直接从上一行的位置直接移下来。
一种是当前状态和上一状态不同,加了一个价值v[i],放在表格中,就是相差了 w 列,再在那个单元格中加上 v[i]

那么,我们可以得到我们的状态转移方程

f[i][j] = f[i - 1][j](j < w[i])时
f[i][j] = max(f[i-1][j],f[i -1][j - w[i]] + v[i])(j >= w[i])

第二个式子就是取不放入背包和放入背包两种情况的最大值

边界条件:
f[i][j] = 0
初始化为0,也就是一开始都没有放入背包

1.3 代码



for(int i = 1;i <= n;i++)   // 物品 i 
{
	for(int j = 1;j <= m;j++) // 容量 j 
	{
		if(j < w[i]) //背包容量小于第i件物品的重量
		{
			f[i][j] = f[i - 1][j];//就复制上一行的价值
		}
		else//否则就取放入和不放入的最大值
		{
			f[i][j] = max(f[i-1][j],f[i - 1][j - w[i]] + v[i]);
 		}
	}	
}


我们是从 1 开始循环,理由很简单,就是 第 0 件物品 和 背包容量为 0 时,价值都是 0 ,我们没必要从 0 开始循环。

1.3.1 代码模拟

当物品重量为 3 ,价值为 5
放入第一件物品 也就是是 i = 1,时,我们观察 随着 j 变化发生了什么
j = 1,放不下,直接从上一行复制值
j = 2,放不下,直接从上一行复制值
j = 3,可以放进来,价值为 5 ,f[1][3] = f[0][0] + 5 = 5
j = 4,可以放进来,价值为5, f[1][4] = f[0][1] + 5 = 5
j = 5,可以放进来,价值为5,f[1][5] = f[0][2] + 5 = 5
j = 6,可以放进来,价值为5,f[1][6] = f[0][3] + 5 = 5

我们可以看到,当 物品可以放进来时该单元格的值,可以从上一行列数相差 w 的单元格来推出。
在这里插入图片描述
我们继续模拟
放入 i = 2 时,也就是放入第 2 件物品时,我们观察随着 j 的变化发生了什么

j = 1 时,放不下 f[2][1]=f[1][1] = 0
j = 2 时,可以放下,价值为 3 f[2][2]=f[1][0] + 3 = 3
j = 3 时,可以放下,但是是直接从上一行拷贝下来

为什么可以放下却是直接从上一行拷贝下来?
因为我这里能放下时,只能放下价值为 3 的物品,那不如直接放价值更大的5,
所以直接拷贝下来
代码里的 max 函数可以体现出来

j = 4 时,可以放下,但是是直接从上一行拷贝下来
j = 5时,可以放下,第一件物品和第二件物品,价值加起来为 8
j = 6时,同理
在这里插入图片描述
可以看到,当可以放进来单元格时的值,同样可以利用上一行列数相差为 w 的单元格来推出 或者 上一行的值来推出

我们继续模拟
当 i = 3 ,也就是放入第三件物品时,我们观察随着 j 的变化发生了什么?
j = 1,时,放不下,直接从上一行复制值 f[3][1]= f[2][1]=0
j = 2 时,只能放下上次的第二件物品,所以最大值就是第二件物品的价值,直接复制下来
f[3][2]= f[2][2]=3;
j = 3时,可以放下 第二件物品 。f[3][3] = f[2][3] = 5,直接从上一行复制下来
j = 4时,可以放下 第三件物品,f[3][4]=f[2][0] + 6=6
j = 5时,我们不放入第三件物品,因为价值不如放入第一件物品和第二件物品的价值
所以我们取最大价值 f[3][5]=f[2][5]=8
j=6时,可以判断出最大价值为第二件物品加上第三件物品,从上一行列数相差为 w 的单元格推出
f[3][6]=f[2][2]+6=9
这样,我们的最大价值就是 f[3][6]=9
在这里插入图片描述

1.4 空间复杂度的优化

再来回顾以下这个代码,是不是豁然开朗

for(int i = 1;i <= n;i++)   // 物品 i 
{
	for(int j = 1;j <= m;j++) // 容量 j 
	{
		if(j < w[i]) //背包容量小于第i件物品的重量
		{
			f[i][j] = f[i - 1][j];//就复制上一行的价值
		}
		else//否则就取放入和不放入的最大值
		{
			f[i][j] = max(f[i-1][j],f[i - 1][j - w[i]] + v[i]);
 		}
	}	
}

这段代码时间复杂度为 o(nm),基本上不能再优化了
但是空间复杂度为 o(n
m),空间复杂度却可以再优化一下
由二维降为一维

在这里插入图片描述
通过刚才代码模拟我们可以发现,每次第i行都是从上一行更新数据,那么当第i行数据更新完成后,上一行是不是可以不用保存了?

1.4.1 错误的优化方式

让一维数组f[j] 只记录一行数据,让j值顺序循环,循环更新f[j]的值会怎么样

来看一下这段错误的代码

for(int i = 1;i <= n;i++)
   {
   		for(int j = 1;j <= m;j++)
   		{
   			if(j < w[i])
   			{
   				f[j] = f[j];
			}
			else
			{
				f[j] = max(f[j],f[j - w[i]] + v[i]);
			}
		}
   }

我们模拟一下
在这里插入图片描述
当 i = 1,也就是放入第 i 件物品时,我们管擦随着j变化发生了什么?
j = 1,放不下 f[1] = 0;
j = 2,放不下 f[2] = 0;
j = 3,可以放下 f[3] = f[0] + 5 = 5;
j = 4,可以放下 f[4] = f[1] + 5 = 5;可是这里的f[1]是我们本行就已经更新值了,拿本行的值继续更新会有什么问题?
j= 5,可以放下 f[5] = f[2] + 5 = 5 ;这里同理,f[2]是我们本行更新的值
问题来了
j = 6,f[6] = f[3] + 5 = 10 出错了,原因就是 f[3] 的值是在我们本行更新的,这样子顺序更新后面肯定会出错!

因为f[j]是顺序循环,f[j - w[i]] 会先于 f[j]更新,那么我们用 新值f[j - w[i]] 的值取更新 f[j],就会出错

1.4.2 正确的优化方式

既然 f[j] 顺序更新不行,那我们就反过来,逆序更新嘛
让 f[j] 逆序循环,让 f[j] 先于 f[j - w[i]] 更新,用旧值 f[j - w[i]] 去更新 f[j]
旧值 f[j - w[i]] 相当于上一行的数,所以优化思路正确


   for(int i = 1;i <= n;i++)
   {
   		for(int j = m;j >= 1 ;j--)
   		{
   			if(j < w[i])
   			{
   				f[j] = f[j];
			}
			else
			{
				f[j] = max(f[j],f[j - w[i]] + v[i]);
			}
		}
   }

我们模拟一下这段代码
在这里插入图片描述
当 i = 1,放入第 1 件物品时,我们观察随着 j 变小,会发生什么?
j = 6时,可以放下,f[6] = f[3] + 5 = 5
j = 5时,可以放下,f[5] = f[2] + 5 = 5
j = 4时,可以放下,f[4] = f[1] + 5 = 5
j = 3时,可以放下,f[3] = f[0] + 5 = 5 ,可以看到,我们f[3] 后面才更新,避免了顺序更新那样提前更新导致后面的数据出错的情况
j = 2,放不下 f[2] = f[2] = 0
j = 1, 放不下 f[1] = f[1] = 0

当 i = 2 ,放入第 2 件物品时,
j = 6 时,可以放下,f[6] = f[4] + 3 = 5 + 3 = 8;

以此类推,就不再继续模拟了

因为 f[j] 是逆序循环,f[j] 会先于 f[j - w[i]] 更新,也就是说,用旧值,f[j - w[i]] 去更新 f[j],
相当于用上一行的 f[j - w[i]] 去更新 f[j],所以思路正确

这里的 f[j] 循环一遍,值就会滚动更新一遍,所以 f[j] 也称为滚动数组
我们用了滚动数组,把空间复杂度从二维降到了一维

1.5 终极版优化

我们看一下之前的代码看看哪里可以优化

   for(int i = 1;i <= n;i++)
   {
   		for(int j = m;j >= 1 ;j--)
   		{
   			if(j < w[i])
   			{
   				f[j] = f[j];   //这里可以优化
			}
			else
			{     //如果上面的if 省略 这里的j - w[i] 可以出现负数,导致出错
				f[j] = max(f[j],f[j - w[i]] + v[i]);
			}
		}
   }

经过几番周折,终于磨练出了下面这份近乎完美的代码

for(int i = 1;i <= n;i++)   // 物品 i 
	{                // 把j的下限改为 w[i]
		for(int j = m;j >= w[i];j--) // 容量 j 
		{
			f[j] = max(f[j],f[j - w[i]] + v[i]);
		}	
    }

在这里插入图片描述

总结

动态规划的题目分析思路
1.确定状态变量(函数)
2.确定状态转移方程
3.确定边界条件
4.确定递推顺序

背包问题状态转移方程

f[j] = max(f[j],f[j - w[i]] + v[i]) 

这个方程非常重要,一定要记住!

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

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

相关文章

Linux下git和gdb的使用

&#x1f680;每日鸡汤&#xff1a;生活不相信眼泪&#xff0c;即使你把眼泪流成珍珠&#xff0c;灰暗的生活也不会因此而闪光。 目录 一、使用git命令行 1.1安装git、配置仓库 Ⅰ.gitignore Ⅱ.git 1.2git的基本使用 二、Linux调试器-gdb 2.1、gdb的使用 2.2、 debug与…

矩阵求导简记

很多机器学习算法都需要求解最值&#xff0c;比如最小二乘法求解样本空间相对拟合曲线的最短距离&#xff0c;最值的求解往往通过求导来计算&#xff0c;而机器学习中又常用矩阵来处理数据&#xff0c;所以很多时候会涉及到矩阵的求导。矩阵求导就像是线性代数和微积分的结合&a…

熬夜肝出囊括Java后端95%的面试题解析

为大家整理了一版java高频面试题&#xff0c;其实&#xff0c;一直有大佬在面试&#xff0c;不是在面试&#xff0c;就是在面试的路上&#xff0c;2022其实不是个适合跳槽的年份&#xff0c;稳稳当当当然好&#xff0c;但是&#xff0c;也别委屈自己呀&#xff0c;话不多说&…

Kotlin编程实战——与Java互操作(10)

一 概述 Kotlin 中调用 Java 代码Java 中调用 Kotlin 二 Kotlin 中调用 Java 代码 Getter 和 Setter返回 void 的方法将 Kotlin 中是关键字的 Java 标识符进行转义空安全与平台类型Java类型映射kotlin类型Kotlin 中的 Java 泛型Java 可变参数 三 Java 中调用 Kotlin 属性实…

【ELM预测】基于matlab探路者算法优化极限学习机预测(含前后对比)【含Matlab源码 2204期】

一、探路者算法简介 提出的一种新兴的智能优化算法&#xff0c;该算法的思想起源于群体动物的狩猎行为&#xff0c;种群中的个体分为探路者和跟随者两种角色。算法的寻优过程模拟了种群寻找食物的探索过程&#xff0c;利用探路者、跟随者两种角色不同的位置更新方式以及角色间…

NR/5G - PUSCH repetition次数

--- R15 DCI format 0-1 PUSCH 38.214中的描述&#xff0c;DCI format 0-1调度的PUSCH&#xff0c;包括C-RNTI/MCS-C-RNTI动态DCI调度PUSCH以及CS-RNTI&#xff0c;NDI1时候指示的Configured Grant的重传调度PUSCH&#xff0c;通过PUSCH-Config中的pusch-AggregationFactor指示…

谷粒学院——Day02【环境搭建和讲师管理接口开发】

前后端分离概念 传统单体结构 前后端分离结构 前后端分离就是将一个单体应用拆分成两个独立的应用&#xff1a;前端应用和后端应用&#xff0c;以JSON格式进行数据交互。 后台讲师管理模块环境搭建 一、数据库设计 数据库 guli_edu 数据库 guli_edu.sql # # Structure fo…

3.1 Python 字符串类型常用操作及内置方法

文章目录1. 类型转换2. 字符串索引取值3. 遍历字符串4. 统计长度5. 字符串的复制与拼接5.1 字符串的复制5.2 加号拼接5.3 .join 方法拼接字符串6. 字符比较7. 成员运算8. .format9. .split10. .strip11 . .upper 与 .lower12. .isupper 与 .islower13. .startswith 与 .endswit…

15 个机器学习的基本 Python 库

一定有很多次你试图在 Python 中找到一个库来帮助你完成机器学习项目。但是&#xff0c;经常遇到一件事&#xff01;今天有如此多的 Python 库可用&#xff0c;并且许多库在每几年之后都会大量发布&#xff0c;因此选择合适的库并不容易。 有时会花费数小时寻找合适的库&#…

【数据结构基础】之图的介绍,生动形象,通俗易懂,算法入门必看

前言 本文为数据结构基础【图】 相关知识&#xff0c;下边将对图的基本概念&#xff0c;图的存储结构&#xff0c;图的遍历包含广度优先遍历和深度优先遍历&#xff0c;循环遍历数组&#xff0c;最小生成树&#xff0c;拓扑排序等进行详尽介绍~ &#x1f4cc;博主主页&#xf…

spring启动流程(二):包的扫描流程

在applicationContext的创建中&#xff0c;我们分析了applicationContext的创建过程&#xff0c;在本文中&#xff0c;我们将分析spring是如何进行包扫描的。 依旧是AnnotationConfigApplicationContext的构造方法&#xff1a; public AnnotationConfigApplicationContext(St…

自底向上语法分析(bottom-up parsing)

自底向上语法分析&#xff08;bottom-up parsing&#xff09;自底向上分析概述LR分析概述LR(0)分析增广文法点标记项目LR(0)分析表CLOSURE函数GOTO函数LR(0)自动机的状态集LR(0)分析表构造算法LR(0)自动机的形式化定义LR(0)分析的冲突问题SLR分析SLR算法的关键SLR分析的冲突问题…

U3D热更新技术

作者 : SYFStrive 博客首页 : HomePage &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f937;‍♀️&#xff1a;创作不易转发需经作者同意&#x1f608; &#x1f483;&#xff1a;程…

适用于 Windows 的企业级 Subversion 服务器

适用于 Windows 的企业级 Subversion 服务器。 Subversion 的 Windows 身份验证 Windows 身份验证是 VisualSVN 服务器的一个关键特性。此功能专为 Active Directory 域环境设计&#xff0c;允许用户使用其 Windows 凭据访问 VisualSVN 服务器。 VisualSVN Server 支持两种不同…

【Linux】基础IO ——中

&#x1f387;Linux&#xff1a;基础IO 博客主页&#xff1a;一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 看似不起波澜的日复一日&#xff0c;一定会在某一天让你看…

这些Java基础知识,诸佬们都还记得嘛(学习,复习,面试都可)

前言&#xff1a;大家好&#xff0c;我是小威&#xff0c;24届毕业生&#xff0c;在一家满意的公司实习。本篇将记录几次面试中经常被问到的知识点以及对学习的知识点总结和面试题的复盘。 本篇文章记录的基础知识&#xff0c;适合在学Java的小白&#xff0c;也适合复习中&…

趣说 Mysql内存篇 Buffer Pool

讲解顺序 先说 Mysql InnoDB 内存结构 Buffer PoolPage 管理机制Change BufferLog Buffer Buffer Pool 接上回 说到了 LRU 算法对内存的数据 进行淘汰 LRU 算法本身是 最近最少使用的&#xff0c;但是这样就会出现 分不清楚 哪些是真正多次使用的数据 LRU缺点&#xff1a…

软考重点10 知识产权

软考重点10 知识产权一、著作权1. 著作权的理解&#xff08;1&#xff09;版权&#xff1a;&#xff08;2&#xff09;人身权与财产权2. 知识产权的归属判定3. 知识产权的归属判定&#xff08;1&#xff09;委托创作&#xff08;2&#xff09;合作开发4. 著作权保护对象及范围5…

为什么要有包装类,顺便说一说基本数据类型、包装类、String类该如何转换?

一、前言 开门见山&#xff0c;首先看看八种基本数据类型对应的包装类&#xff1a; 基本数据类型包装类charCharacterbyteByteshortShortintIntegerlongLongfloatFloatdoubleDoublebooleanBoolean 其中Character 、Boolean的父类是Object&#xff0c;其余的父类是Number 二、装…

【软件测试】毕业打工两年,辞职一年后转行月薪18K,软件测试让我发起了第一春......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 小徐&#xff1a; 毕…