算法——动态规划:0/1 背包问题
文章目录
- 一、问题描述
- 二、解决方案
- 1. DP 状态的设计
- 2. 状态转移方程
- 3. 算法复杂度
- 4. 举例
- 5. 实现
- 6. 滚动数组
- 6.1 两行实现
- 6.2 单行实现
- 6.3 优缺点
- 三、总结
一、问题描述
问题的抽象:给定 n n n 种物品和一个背包,第 i i i 个物品的体积为 c i c_i ci,价值为 w i w_i wi,背包的总容量为 C C C。把物品装入背包时,第 i i i 种物品只有两种选择:装入背包或不装入背包。如何选择装入背包的物品,使装入背包中的物品的总价值最大?
具体的问题可以看这道洛谷题:P1048 [NOIP2005 普及组] 采药,将 物品 换成了 草药,将 容量 换成了 时间,将 背包的容量 换成了 规定的时间。
二、解决方案
0/1 背包问题 是一道 经典 的使用 动态规划 思想的题目,同时也不是很难,掌握它之后就正式跨入 动态规划 的大门了。
1. DP 状态的设计
引入一个 ( N + 1 ) × ( C + 1 ) (N + 1) \times (C + 1) (N+1)×(C+1) 的二维数组 d p [ ] [ ] dp[][] dp[][],称为 DP 状态。其中, d p [ i ] [ j ] dp[i][j] dp[i][j] 表示把前 i i i 个(从第 1 1 1 个到第 i i i 个)物品装入容量为 j j j 的背包中获得的最大价值。
每个 d p [ i ] [ j ] dp[i][j] dp[i][j] 都是一个 0/1 背包问题:将前 i i i 个物品装入容量为 j j j 的背包。所以 d p [ N ] [ C ] dp[N][C] dp[N][C] 就代表将前 N N N 个物品装入容量为 C C C 的背包。
2. 状态转移方程
状态转移方程指的是 从一个状态转变到另一个状态的递推公式。
一般使用 自底向上 的 递推 来解决背包问题,假设已经递推到 d p [ i ] [ j ] dp[i][j] dp[i][j],分两种情况:
- 第 i i i 个物品的体积比容量 j j j 还大,不能装进容量为 j j j 的背包。此时直接继承前 i − 1 i - 1 i−1 个物品装进容量 j j j 的背包的情况即可,即 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i - 1][j] dp[i][j]=dp[i−1][j]。
- 第 i i i 个物品的体积比容量 j j j 小,能装进背包。此时就体现 0/1 了( 0 0 0 表示不装, 1 1 1 表示装):
- 装第 i i i 个物品。先在 前 i − 1 i - 1 i−1 个物品装进容量为 j j j 的背包中,给第 i i i 个物品空出 c i c_i ci 的空间,从而得到背包 d p [ i − 1 ] [ j − c [ i ] ] dp[i - 1][j - c[i]] dp[i−1][j−c[i]]。然后将第 i i i 个物品放入这个背包,背包的价值增加 w i w_i wi,从而有当前背包 d p [ i ] [ j ] = d p [ i − 1 ] [ j − c [ i ] ] + w [ i ] dp[i][j] = dp[i - 1][j - c[i]] + w[i] dp[i][j]=dp[i−1][j−c[i]]+w[i]。
- 不装第 i i i 个物品。直接继承前 i − 1 i - 1 i−1 个物品装进容量 j j j 的背包的情况即可,即 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i - 1][j] dp[i][j]=dp[i−1][j]。
- 此时取这两者中的较大值作为当前背包的价值,状态转移方程为: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − c [ i ] ] + w [ i ] ) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + w[i]) dp[i][j]=max(dp[i−1][j],dp[i−1][j−c[i]]+w[i])
所以 0/1 背包问题 的特征如下:
- 重叠子问题是 d p [ i ] [ j ] dp[i][j] dp[i][j]
- 最优子结构是 d p [ i ] [ j ] dp[i][j] dp[i][j] 的状态转移方程。
3. 算法复杂度
- 时间复杂度:在计算时,需要计算二维矩阵 d p [ ] [ ] dp[][] dp[][] 中的每一个值,矩阵的大小是 N C NC NC,每次计算的时间为 O ( 1 ) O(1) O(1),所以时间复杂度为 O ( N C ) O(NC) O(NC)。
- 空间复杂度:使用了大小为 N C NC NC 的二维数组,所以空间复杂度为 O ( N C ) O(NC) O(NC)。
说明:如果 N , C N, C N,C 很大,则 N + 1 , C + 1 N + 1, C + 1 N+1,C+1 可以近似地看作 N , C N, C N,C,为了简化,所以复杂度不是 O ( ( N + 1 ) × ( C + 1 ) ) O((N + 1) \times (C + 1)) O((N+1)×(C+1))。
4. 举例
初学者可能很难理解,这时举一个实际案例就懂了。对于 P1048 题,有一个测试用例:背包的容量为 70,物品的数量为 3,物品的体积数组为 [71, 69, 1],物品的价值数组为 [100, 1, 2]。
定义一个 4 * 71 的二维数组,如下所示(由于长度限制,中间的数全部用 ... 代替,... 代表的数 和 ... 两边的数相同):

先填充索引为 1 的行(第一个物品的体积为 71,价值为 100):

然后填充索引为 2 的行(第二个物品的体积为 69,价值为 1):

接着填充索引为 3 的行(第三个物品的体积为 1,价值为 2):

最终,数组的 dp[3][70] 的位置存储着题目的结果——将前 3 个物品放入容量为 70 的背包的最大价值。
5. 实现
// 虽然这些代码看起来是 C 语言的代码,但如果选择 C 语言,则会编译失败。建议选择 C++14
#include <stdio.h>const int MAX_N = 105; // 最大的物品数量,与题目有关
const int MAX_C = 1005; // 最大的背包容量,与题目有关
int N, C; // N 是物品的数量,C 是背包的容量
int c[MAX_N], w[MAX_N]; // c[i], w[i] 分别表示第 i 个物品的 体积 和 价值
int dp[MAX_N][MAX_C]; // dp[i][j] 表示将前 i 个物品装入容量为 j 的背包中// 取 x 和 y 中的较大值
int max(int x, int y) {return x > y ? x : y;
}// 进行动态规划
int programming() {for (int i = 1; i <= N; i++) {for (int j = 1; j <= C; j++) {if (j < c[i]) { // 装不下第 i 个物品dp[i][j] = dp[i - 1][j];} else { // 能装下第 i 个物品dp[i][j] = max(dp[i - 1][j - c[i]] + w[i], // 装第 i 个物品dp[i - 1][j] // 不装第 i 个物品);}}}return dp[N][C];
}int main() {// 读取数据scanf("%d %d", &C, &N);for (int i = 1; i <= N; i++) {scanf("%d %d", &c[i], &w[i]);}// 进行动态规划printf("%d", programming());return 0;
}
6. 滚动数组
滚动数组 是 动态规划 最常用的 空间优化技术。动态规划的状态方程通常是二维即以上,占用了太多空间,用滚动数组可以 极大程度上 减少空间占用,它能把 二维 状态方程的空间复杂度优化到 一维,更高维的数组也可以通过优化,减少一个维度。
从状态转移方程 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − c [ i ] ] + w [ i ] ) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + w[i]) dp[i][j]=max(dp[i−1][j],dp[i−1][j−c[i]]+w[i]) 中可以看出, d p [ i ] [ ] dp[i][] dp[i][] 只与 d p [ i − 1 ] [ ] dp[i - 1][] dp[i−1][] 有关,与之前的 d p [ i − 2 ] [ ] , d p [ i − 3 ] [ ] , ⋯ dp[i - 2][], dp[i - 3][], \cdots dp[i−2][],dp[i−3][],⋯ 都无关。由于它们都没用了,索性就 复用 它们占用的空间,用新的一行覆盖已经无用的一行,只需要两行就够了。这就是“滚动”的含义。
滚动数组根据使用的行数不同,分为两种实现方式:
6.1 两行实现
仍然使用二维数组,不过不是 ( N + 1 ) × ( C + 1 ) (N + 1) \times (C + 1) (N+1)×(C+1) 的二维数组,而是 2 × ( C + 1 ) 2 \times (C + 1) 2×(C+1) 的二维数组,这就是所谓的“两行”:计算 本行 时用 上一行 的结果,然后将 本行 和 上一行 互换,计算 新的本行(新的本行 使用了 原来上一行 的内存空间)时使用 新的上一行(新的上一行 使用了 原来本行 的内存空间)。
#include <stdio.h>const int MAX_N = 105; // 最大的物品数量,与题目有关
const int MAX_C = 1005; // 最大的背包容量,与题目有关
int N, C; // N 是物品的数量,C 是背包的容量
int c[MAX_N], w[MAX_N]; // c[i], w[i] 分别表示第 i 个物品的 体积 和 价值
int dp[2][MAX_C]; // 使用两行滚动数组,dp[i][j] 成为不断更新的变量,没有固定的含义// 取 x 和 y 中的较大值
int max(int x, int y) {return x > y ? x : y;
}// 交换 i 和 j 的值
// 注意 &,它表示引用传递,使用传入的实参,而不是重新创建一个新变量来代表实参,这是 C++ 的特性
void swap(int &i, int &j) {int temp = i;i = j;j = temp;
}// 进行动态规划
int programming() {/*在状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + w[i]) 中,位于 dp 的行索引的 i 用 curr 来代替,i - 1 用 prev 来代替prev, curr 分别是 上一行 和 本行 索引*/int prev = 0, curr = 1;for (int i = 1; i <= N; i++) {swap(prev, curr); // 将本行和上一行互换for (int j = 1; j <= C; j++) {if (j < c[i]) { // 装不下第 i 个物品dp[curr][j] = dp[prev][j];} else { // 能装下第 i 个物品dp[curr][j] = max(dp[prev][j - c[i]] + w[i], // 装第 i 个物品dp[prev][j] // 不装第 i 个物品);}}}return dp[curr][C]; // 返回当前行的最后一个值
}int main() {// 读取数据scanf("%d %d", &C, &N);for (int i = 1; i <= N; i++) {scanf("%d %d", &c[i], &w[i]);}// 进行动态规划printf("%d", programming());return 0;
}
6.2 单行实现
实际上还能省,发现计算 d p [ i ] [ j ] dp[i][j] dp[i][j] 时会用到 d p [ i − 1 ] [ j − c [ i ] ] dp[i - 1][j - c[i]] dp[i−1][j−c[i]] 和 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i−1][j],所以还可以复用 上一行 中的部分内存空间。
由于 j − c [ i ] j - c[i] j−c[i] 不是一个确定的值,取值范围为 [ 0 , j ) [0, j) [0,j),所以 无法 在计算出 d p [ i ] [ j ] dp[i][j] dp[i][j] 的结果前 复用 上一行 中 j j j 之前的内存空间。既然如此,就考虑 复用 上一行 中 j j j 之后的内存空间。此时能想到:如果 从后向前计算 d p [ i ] [ j ] dp[i][j] dp[i][j],就不会影响 j j j 之前的值。进而就有 单行 的滚动数组实现:
#include <stdio.h>const int MAX_N = 105; // 最大的物品数量,与题目有关
const int MAX_C = 1005; // 最大的背包容量,与题目有关
int N, C; // N 是物品的数量,C 是背包的容量
int c[MAX_N], w[MAX_N]; // c[i], w[i] 分别表示第 i 个物品的 体积 和 价值
int dp[MAX_C]; // 使用单行滚动数组,dp[j] 成为不断更新的变量,没有固定的含义// 取 x 和 y 中的较大值
int max(int x, int y) {return x > y ? x : y;
}// 进行动态规划
int programming() {for (int i = 1; i <= N; i++) {for (int j = C; j >= 1; j--) { // 切记要从后往前计算if (j < c[i]) { // 装不下第 i 个物品dp[j] = dp[j];} else { // 能装下第 i 个物品dp[j] = max(dp[j - c[i]] + w[i], // 装第 i 个物品dp[j] // 不装第 i 个物品);}}}return dp[C]; // 返回单行的最后一个值
}int main() {// 读取数据scanf("%d %d", &C, &N);for (int i = 1; i <= N; i++) {scanf("%d %d", &c[i], &w[i]);}// 进行动态规划printf("%d", programming());return 0;
}
6.3 优缺点
- 优点:优化了空间复杂度,很大程度上减少了内存的使用。
- 缺点:
- 由于复用了空间,从而覆盖了之前的计算结果,数组中的值没有固定的实际含义,难以理解,通常都是先写出常规的动态规划,再使用滚动数组进行优化。
- 还是因为复用了空间,导致只留下最后一次计算的状态,无法从数组中看出具体的方案。
三、总结
0/1 背包问题 的 0/1 在于每种物品只有两种状态——放进背包 和 不放进背包,针对这一问题,使用了 动态规划 的解决方案,由于空间复杂度比较高,所以还使用 滚动数组 的思想进行优化,从而将占用的空间 降维。初学者适合用 两行 的实现,不容易犯错。熟练掌握动态规划后,可以使用 单行 的实现。
相关文章:
算法——动态规划:0/1 背包问题
文章目录 一、问题描述二、解决方案1. DP 状态的设计2. 状态转移方程3. 算法复杂度4. 举例5. 实现6. 滚动数组6.1 两行实现6.2 单行实现6.3 优缺点 三、总结 一、问题描述 问题的抽象:给定 n n n 种物品和一个背包,第 i i i 个物品的体积为 c i c_i …...
又是奇瑞,“统一下班时间”过去不久,最近又整新活了...
奇瑞 345 345 可不是奇瑞的汽车型号,而是奇瑞 7 月份会议文章中提出的新策略。 简单来说,要提高加班效率,实现 3 个人干 5 个人活,拿 4 个人的工资,要把员工当成家人一样看待,要对他们的健康幸福负责。 前面…...
ubuntu24.04lts cmake编译 opencv4.5.4 contrib的一些问题
编译之前一定要安装好必须的库,否则即使提示编译成功,调用opencv后也可能会有问题 sudo apt-get update sudo apt-get upgradesudo apt-get install -y g sudo apt-get install -y cmake sudo apt-get install -y make sudo apt-get install…...
大数据面试SQL(三):每分钟在线直播人数
文章目录 每分钟在线直播人数 一、题目 二、分析 三、SQL实战 四、样例数据参考 每分钟在线直播人数 一、题目 有如下数据记录直播平台主播上播及下播时间,根据该数据计算出平台每分钟的在线直播人数。 这里用主播名称做统计,前提是主播名称唯一…...
python中执行mysql操作并将python脚本共享
mysql下载路径: MySQL :: MySQL Community Downloads [root2 ~]# vim py001.py a3 b4 print(ab) print(a**2b**2) [root2 ~]# python py001.py 7 25 [root2 ~]# python3 >>> import random >>> random <module rando…...
HTTP、HTTPS、SOCKS5三种协议特点
在互联网通信中,HTTP、HTTPS和SOCKS5是三种至关重要的协议,它们各自具有独特的特点和应用场景。本文将详细探讨这三种协议的特点,帮助读者更好地理解它们在网络通信中的作用。 一、HTTP协议特点 HTTP(Hypertext Transfer Protoc…...
在ubuntu、centos、openEuler安装Docker
目录 ubuntu、centos、openEuler安装Docker 1.在 Ubuntu 上安装 Docker 1. 1 更新软件包 1. 2 安装必要的依赖 1.3 添加 Docker 的 GPG 密钥 1.4 添加 Docker 仓库 1.5 更新软件包 1.6 安装 Docker 1.7 启动并启用 Docker 服务 1.8 验证安装 1.9 运行测试容器 1.10…...
公共命名空间的例子3
有这样一个句子 用x语言解释[12*3]。 在x语言中,不符合“先乘除后加减”,这个句子应该怎样解释呢? 第一步,进行词法分析,目的是识别出注释和字符串,其中可能包括任意符号,干扰编译过程。 第二步…...
【云存储】SDS软件定义存储,数据存储的类型与技术方案(块/文件/对象,Ceph、RBD等)
【云存储】SDS软件定义存储,数据存储的类型与技术方案(块/文件/对象,Ceph、RBD等) 文章目录 1、分布式存储架构(软件定义存储SDS,超融合基础架构HCI)2、存储类型(块存储,…...
第31课 Scratch入门篇:小画家(舞台上画画)
小画家(舞台上画画) 故事背景: 在舞台上选择画笔和颜色,进行画画 程序原理: 这节课我们继续练习画笔功能,通过画笔功能我们设计一个小画板,碰到哪种颜色画笔就切换成哪种颜色。 开始编程 1、绘制一大一小的黑色圆形,小的命名为画笔,大的圆形命名为black(黑色) 2、鼠…...
QT UI界面之ListView
文章目录 概述源码怎么用代码qt design 小结 概述 本来把布局文件那块写了一遍,但是看看都跟之前那篇差不多,就换了一个稍微有点难度的,也很常用的listview来写了。来看看,有什么好玩的。 源码 先看下源码,如下&…...
freeRTOS互斥量(mutex)
目录 前言 一、互斥量概述 二、互斥量函数 1.创建 2.其他函数 三、优先级反转示例 1.概念 2.代码示例 四、优先级继承 1.概念 2.代码示例 五、递归锁 1.死锁的概念 2.自我死锁 3.函数 4.递归锁代码示例 前言 在之前的信号量中,我们想要实现互斥的…...
基于GeoTools使用JavaFx进行矢量数据可视化实战
目录 前言 一、JavaFx展示原理说明 二、GeoTools的Maven依赖问题 三、引入Geotools相关的资源包 四、创建JavaFx的Canvas实例 五、JavaFx的Scene和Node的绑定 六、总结 前言 众所周知,JavaFx是Java继Swing之后的又一款用于桌面应用的开发利器。当然࿰…...
zabbix的setup无法进入第二步
注意-部署时,报错要看的日志不止一个,php日志的报错也要看的,nginx接收到请求后是转发到php-fpm的 [rootweb01-84-41 ~]# chmod -R 777 /var/lib/php/session chmod: 无法访问"/var/lib/php/session": 没有那个文件或目录 [rootweb…...
代码随想录算法训练营第四十六天 | 115. 不同的子序列、583. 两个字符串的删除操作、72. 编辑距离、编辑距离总结篇
一、115. 不同的子序列 题目链接:115. 不同的子序列 - 力扣(LeetCode) 文章讲解:代码随想录 (programmercarl.com)——115. 不同的子序列 视频讲解:动态规划之子序列,为了编辑距离做铺垫 | LeetCode&#x…...
宝塔安装nginx失败报错“检测到系统组件wget不存在,无法继续安装”
宝塔安装nginx失败报错“检测到系统组件wget不存在,无法继续安装” 问题描述解决方案 问题描述 在宝塔中安装lnmp环境时,安装nginx失败报错:检测到系统组件wget不存在,无法继续安装 如下图所示 通过检查发现系统是已经安装了wge…...
C++之运算符重载系列深入学习:从入门到精通!
为什么需要对运算符进行重载 C预定义中的运算符的操作对象只局限于基本的内置数据类型,但是对于我们自定义的类型是没有办法操作的。但是大多时候我们需要对我们定义的类型进行类似的运算,这个时候就需要我们对这么运算符进行重新定义,赋予其…...
国赛分析。。。。
山东 6散落2两元素 浙江 8散落两元素 安徽不公布 4散落2元素 120s 华南 8散落两元素 西部 8散落两元素 华北 8 2.。。。 华东 东北 路边6张两元素...
无缝融入,即刻智能[一]:Dify-LLM大模型平台,零编码集成嵌入第三方系统,42K+星标见证专属智能方案
无缝融入,即刻智能[一]:Dify-LLM大模型平台,零编码集成嵌入第三方系统,42K+星标见证专属智能方案 1.Dify 简介 1.1 功能情况 Dify,一款引领未来的开源大语言模型(LLM)应用开发平台,革新性地融合了后端即服务(Backend as a Service,BaaS)与LLMOps的精髓,为开发者铺…...
PLSQL导入导出ORACLE数据提示失败问题修改PLSQL配置
oracle中plsql导入提示无法导入问题 1.首先看下是否环境变量已经配置(具体配置看下面环境变量配置) 2.plsql数据导入中tools-->Preferences中配置如下框中的内容 3.设置 tnsnames.ora文件中看下是否设置有问题 4.PLSQL乱码问题 NLS_LANG SIMPLIFIED CHINESE_CHINA.ZHS16…...
未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...
【JVM】- 内存结构
引言 JVM:Java Virtual Machine 定义:Java虚拟机,Java二进制字节码的运行环境好处: 一次编写,到处运行自动内存管理,垃圾回收的功能数组下标越界检查(会抛异常,不会覆盖到其他代码…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...
3-11单元格区域边界定位(End属性)学习笔记
返回一个Range 对象,只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意:它移动的位置必须是相连的有内容的单元格…...
python执行测试用例,allure报乱码且未成功生成报告
allure执行测试用例时显示乱码:‘allure’ �����ڲ����ⲿ���Ҳ���ǿ�&am…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习)
Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习) 一、Aspose.PDF 简介二、说明(⚠️仅供学习与研究使用)三、技术流程总览四、准备工作1. 下载 Jar 包2. Maven 项目依赖配置 五、字节码修改实现代码&#…...
解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用
在工业制造领域,无损检测(NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统,以非接触式光学麦克风技术为核心,打破传统检测瓶颈,为半导体、航空航天、汽车制造等行业提供了高灵敏…...
Linux中《基础IO》详细介绍
目录 理解"文件"狭义理解广义理解文件操作的归类认知系统角度文件类别 回顾C文件接口打开文件写文件读文件稍作修改,实现简单cat命令 输出信息到显示器,你有哪些方法stdin & stdout & stderr打开文件的方式 系统⽂件I/O⼀种传递标志位…...
python打卡第47天
昨天代码中注意力热图的部分顺移至今天 知识点回顾: 热力图 作业:对比不同卷积层热图可视化的结果 def visualize_attention_map(model, test_loader, device, class_names, num_samples3):"""可视化模型的注意力热力图,展示模…...
