算法——动态规划: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…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...
Java多线程实现之Callable接口深度解析
Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作
一、上下文切换 即使单核CPU也可以进行多线程执行代码,CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短,所以CPU会不断地切换线程执行,从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

企业如何增强终端安全?
在数字化转型加速的今天,企业的业务运行越来越依赖于终端设备。从员工的笔记本电脑、智能手机,到工厂里的物联网设备、智能传感器,这些终端构成了企业与外部世界连接的 “神经末梢”。然而,随着远程办公的常态化和设备接入的爆炸式…...

【分享】推荐一些办公小工具
1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由:大部分的转换软件需要收费,要么功能不齐全,而开会员又用不了几次浪费钱,借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...

Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...
Vite中定义@软链接
在webpack中可以直接通过符号表示src路径,但是vite中默认不可以。 如何实现: vite中提供了resolve.alias:通过别名在指向一个具体的路径 在vite.config.js中 import { join } from pathexport default defineConfig({plugins: [vue()],//…...

CVPR2025重磅突破:AnomalyAny框架实现单样本生成逼真异常数据,破解视觉检测瓶颈!
本文介绍了一种名为AnomalyAny的创新框架,该方法利用Stable Diffusion的强大生成能力,仅需单个正常样本和文本描述,即可生成逼真且多样化的异常样本,有效解决了视觉异常检测中异常样本稀缺的难题,为工业质检、医疗影像…...
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析 一、第一轮基础概念问题 1. Spring框架的核心容器是什么?它的作用是什么? Spring框架的核心容器是IoC(控制反转)容器。它的主要作用是管理对…...