0x52 背包

0x52 背包

背包是线性DP中一类重要而特殊的模型。

1. 0/1背包

0/1背包问题的模型如下:

给定 N N N个物品,其中第 i i i个物品的体积为 V i V_i Vi,价值为 W i W_i Wi。有一个容积为 M M M的背包,要求选择一些物品放入背包,使得物品总体积不超过 M M M的前提下,物品的价值总和最大。

根据上一节线性DP的知识,很容易想到依次考虑每个物品是否放入背包,用“已经处理的物品数”作为DP的“阶段”,以“背包中已经放入的物品总体积”作为附加维度。

F [ i , j ] F[i,j] F[i,j]表示从前 i i i个物品中选出了若干物品放入体积为 j j j的背包,物品的最大价值和。
F [ i , j ] = max ⁡ { F [ i − 1 , j ] , 不选第 i 个物品 F [ i − 1 , j − V i ] + W i , 选第 i 个物品 F[i, j]=\max \left\{\begin{array}{l} F[i-1, j] ,不选第i个物品 \\ F[i-1, j-V_i]+W_i ,选第i个物品 \end{array}\right. F[i,j]=max{F[i1,j],不选第i个物品F[i1,jVi]+Wi,选第i个物品
初值:未放入物品价值皆0,目标: F [ N ] [ M ] F[N][M] F[N][M]

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

通过DP的状态转移方程,我们发现,每一阶段 i i i的状态只与上一个阶段 i − 1 i-1 i1的状态有关。在这种情况下,可以使用称为“滚动数组”的优化方法,降低空间开销。

int f[2][MAX_M+1];
memset(f,0,sizeof(f));
for(int i=1;i<=n;++i)
{for(int j=0;j<=m;++j){f[i&1][j]=f[(i-1)&1][j];if(j>=v[i])f[i&1][j]=max(f[i&1][j],f[(i-1)&1][j-v[i]]+w[i]);}
}

在上面的程序中,我们把阶段 i i i的状态存储在第一维下标为 i & 1 i\&1 i&1的二维数组中。当 i i i为奇数时, i & 1 i\&1 i&1等于1;当 i i i为偶数时, i & 1 i\&1 i&1等于0。因此,DP的状态就相当于在 F [ 0 ] [ ] F[0][] F[0][] F [ 1 ] [ ] F[1][] F[1][]两个数组中交替转移,空间复杂度从 O ( N M ) O(NM) O(NM)降低为 O ( M ) O(M) O(M)

进一步分析发现,在每一个阶段开始时,实际上执行了一次从 F [ i − 1 ] [ ] F[i-1][] F[i1][] F [ i ] [ ] F[i][] F[i][]的拷贝操作。这提示我们进一步省略掉 F F F数组的第一维,只用一维数组,即当外层循环到 i i i个物品时, F [ j ] F[j] F[j]表示背包中放入总体积为 j j j的物品的最大价值和。

int f[MAX_M+1];
memset(f,0,sizeof(f));
f[0]=0;
for(int i=1;i<=n;++i)for(int j=m;j>=v[i];--j)f[j]=max(f[j],f[j-v[i]]+w[i]);

请注意上面的代码片段特别标注的部分——我们使用的了倒序循环。循环到 j j j时:

1. F F F数组的后半部分 F [ j ∼ M ] F[j\sim M] F[jM]处于“第 i i i个阶段”,也就是已经考虑过放入第 i i i个物品的情况。

2.前半部分 F [ 0 ∼ j − 1 ] F[0\sim j-1] F[0j1]处于“第 i − 1 i-1 i1个阶段”,也就是还没有第 i i i个物品更新。

接下来 j j j不断减小,意味着我们总是用“第 i − 1 i-1 i1个阶段”的状态向“第 i i i个阶段”的状态进行转移,符合线性DP的原则,进而保证了第 i i i个物品只会被放入背包一次。如下图所示。

在这里插入图片描述

然而,如果使用正序循环,假设 F [ j ] F[j] F[j] F [ j − V i ] + W i F[j-V_i]+W_i F[jVi]+Wi更新,接下来 j j j增大到 j + V i j+V_i j+Vi时, F [ j + V i ] F[j+V_i] F[j+Vi]又可能被 F [ j ] + W i F[j]+W_i F[j]+Wi更新。此时,两个都处于“第 i i i个阶段”的状态之间发生了转移,违背了线性DP的原则,相当于第 i i i个物品被使用了两次。如下图所示。

在这里插入图片描述

所以,在上面的代码中必须用到倒序循环,才符合0/1背包中每个物品是唯一的、只能放入背包一次的要求。

2.完全背包

完全背包问题的模型如下:

给定 N N N个物品,其中第 i i i个物品的体积为 V i V_i Vi,价值为 W i W_i Wi,并且有无数个。有一个容积为 M M M的背包,要求选择一些物品放入背包,使得物品总体积不超过 M M M的前提下,物品的价值总和最大。

先来考虑使用传统的二维线性DP的做法。设 F [ i , j ] F[i,j] F[i,j]表示从前 i i i个物品中选出了若干物品放入体积为 j j j的背包,物品的最大价值和。
F [ i , j ] = max ⁡ { F [ i − 1 , j ] , 尚未选过第 i 种物品 F [ i , j − V i ] + W i , i f j ≥ V i , 从第 i 种物品中选一个 F[i,j]=\max \left\{\begin{array}{l} F[i-1,j],尚未选过第i种物品 \\ F[i,j-V_i]+W_i,{ if } \ j\geq V_i,从第i种物品中选一个 \end{array} \right. F[i,j]=max{F[i1,j],尚未选过第i种物品F[i,jVi]+Wi,if jVi,从第i种物品中选一个
初值:未放入物品价值皆0,目标: F [ N ] [ M ] F[N][M] F[N][M]

与0/1背包一样,我们也可以省略 F F F数组的 i i i这一维。根据我们在0/1背包中对循环顺序的分析,当采用正序循环时,就对应这每种物品可以使用无限次,也对应着 F [ i , j ] = F [ i , j − V i ] + W i F[i,j]=F[i,j-V_i]+W_i F[i,j]=F[i,jVi]+Wi这个在两个均处于 i i i阶段的状态之间进行转移的方程。

int f[MAX_M+1];
memset(f,0,sizeof(0));
for(int i=1;i<=n;++i)
{for(int j=v[i];j<=m;++j)f[j]=max(f[j],f[j-v[i]]+w[i]);
}

3.多重背包

多重背包问题的模型如下:

给定 N N N个物品,其中第 i i i个物品的体积为 V i V_i Vi,价值为 W i W_i Wi,并且有 C i C_i Ci个。有一个容积为 M M M的背包,要求选择一些物品放入背包,使得物品总体积不超过 M M M的前提下,物品的价值总和最大。

直接拆分法

求解多重背包问题最直接的方法就是把第 i i i种物品看作独立的 C i C_i Ci个物品,转化为共有 ∑ i = 1 N C i \sum_{i=1}^N C_i i=1NCi个物品的0/1背包问题进行计算,时间复杂度为 O ( M ∗ ∑ i = 1 N C i ) O(M*\sum_{i=1}^N C_i) O(Mi=1NCi)。该算法把每种物品拆分成了 C i C_i Ci个,效率较低。

unsigned int f[MAX_M+1];
memset(f,0,sizeof(f));
for(int i=1;i<=n;++i)for(int j=1;j<=c[i];++j)for(int k=m;k>=v[i];--k)f[k]=max(f[k],f[k-v[i]]+w[i]);

二进制拆分法

众所周知,从 2 0 , 2 1 , 2 2 , . . . , 2 k − 1 2^0,2^1,2^2,...,2^{k-1} 20,21,22,...,2k1 k k k个2的整数次幂中选出若干个相加,可以表示出 0 ∼ 2 k − 1 0\sim 2^k-1 02k1之间的任何整数。进一步地,我们求出满足 2 0 + 2 1 + . . . + 2 p ≤ C i 2^0+2^1+...+2^p \leq C_i 20+21+...+2pCi的最大整数 p p p,设 R i = C i − 2 0 − 2 1 − . . . − 2 p R_i=C_i-2^0-2^1-...-2^p Ri=Ci2021...2p,那么:

1.根据 p p p的最大性,有 2 0 + 2 1 + . . . + 2 p + 1 > C i 2^0+2^1+...+2^{p+1} > C_i 20+21+...+2p+1>Ci,可推出 2 p + 1 > R i 2^{p+1}>R_i 2p+1>Ri,因此 2 0 , 2 1 , . . . , 2 p 2^0,2^1,...,2^{p} 20,21,...,2p选出若干个相加可以表示出 0 ∼ R i 0\sim R_i 0Ri之间的任何整数。

2.从 2 0 , 2 1 , . . . , 2 p 2^0,2^1,...,2^{p} 20,21,...,2p以及 R i R_i Ri中选出若干个相加,可以表示出 R i ∼ R i + 2 p + 1 − 1 R_i\sim R_i+2^{p+1}-1 RiRi+2p+11之间的任何整数,而根据 R i R_i Ri的定义, R i + 2 p + 1 − 1 = C i R_i+2^{p+1}-1=C_i Ri+2p+11=Ci,因此 2 0 , 2 1 , . . . , 2 p , R i 2^0,2^1,...,2^{p},R_i 20,21,...,2p,Ri选出若干个相加可以表示出 R i ∼ C i R_i\sim C_i RiCi之间的任何整数。

综上所述,我们可以把数量 C i C_i Ci的第 i i i种物品拆成 p + 2 p+2 p+2个物品,它们的体积分别为:
2 0 ∗ V i , 2 1 ∗ V i , . . , 2 p ∗ V i , R i ∗ V i 2^0*V_i,2^1*V_i,..,2^p*V_i,R_i*V_i 20Vi,21Vi,..,2pVi,RiVi
p + 2 p+2 p+2个物品可以凑成 0 ∼ C i ∗ V i 0\sim C_i*V_i 0CiVi之间所有能被 V i V_i Vi整除的数,并且不能凑成大于 C i ∗ V i C_i*V_i CiVi的数。这等价于原问题种体积为 V i V_i Vi的物品可以使用 0 ∼ C i 0\sim C_i 0Ci次。该方法仅把每种物品拆成了 O ( l o g C i ) O(logC_i) O(logCi)个,效率较高。

for(int i=1;i<=n;++i)
{int sum=c[i];for(int j=1;j<=sum;j*=2){for(int k=m;k>=j*v[i];--k)f[k]=max(f[k],f[k-j*v[i]]+j*w[i]);sum-=j;}if(sum>0){int j=sum;for(int k=m;k>=j*v[i];--k)f[k]=max(f[k],f[k-j*v[i]]+j*w[i]);}
}

单调队列

使用单调队列优化的动态规划算法求解多重背包问题,时间复杂度可以进一步降低到 O ( N M ) O(NM) O(NM),与0/1背包和完全背包中的DP算法的效率相同,我们将在0x59节中进行讲解。

4.分组背包

分组背包问题的模型如下:

给定 N N N组物品,其中第 i i i组有 C i C_i Ci个物品。第 i i i组的第 j j j个物品的体积为 V i j V_{ij} Vij,价值为 W i j W_{ij} Wij。有一容积为 M M M的背包,要求选择若干个物品放入背包,使得每组至多选择一个物品并且物品总体积不超过 M M M的前提下,物品的价值总和最大。

仍然先考虑原始线性DP的做法。为了满足“每组至多选择一个物品”,很自然的想法就是利用“阶段”线性增长的特征,把“物品组数”作为DP的“阶段”,只要使用了一个第 i i i组的物品,就从第 i i i个阶段的状态转移到第 i + 1 i+1 i+1个阶段的状态。设 F [ i , j ] F[i,j] F[i,j]表示从前 i i i组中选出物品放入总体积为 j j j的背包中,物品的最大价值和。
F [ i , j ] = max ⁡ { F [ i − 1 , j ] , 不选第 i 组的物品 max ⁡ 1 ≤ k ≤ C i F [ i − 1 , j − V i k ] + W i k , 选第 i 组的某个物品 k F[i,j]=\max \left\{\begin{array}{l} F[i-1,j],不选第i组的物品 \\ \underset{1\leq k \leq C_i} \max F[i-1,j-V_{ik}]+W_{ik},选第i组的某个物品k \end{array} \right. F[i,j]=max{F[i1,j],不选第i组的物品1kCimaxF[i1,jVik]+Wik,选第i组的某个物品k
与前面几个背包模型一样,我们可以省略 F F F数组的第一维,用 j j j的倒序循环来控制“阶段 i i i”的状态只能从“阶段 i − 1 i-1 i1”转移而来。

memset(f,0,sizeof(f));
for(int i=1;i<=n;++i)for(int j=m;j>=0;j--)for(int k=1;k<=c[i];++k)if(j>=v[i][k])f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);

除了倒序循环 j j j之外,对于每一组内 c [ i ] c[i] c[i]个物品的循环 k k k应该放在 j j j内层。从背包的角度看,这是因为每组内至多选择一个物品,若把 k k k置于 j j j的外层,就会类似多重背包,每组物品在 F F F数组的转移上会产生累积,最终可以选择超过一个物品。从动态规划的角度, i i i是“阶段”, i i i j j j共同构成“状态”,而 k k k是“决策”——在第 i i i组内使用哪一个物品,这三者的顺序绝对不能混淆。

另外,分组背包是许多树形DP问题中状态转移的基本模型,在0x54节中将进一步接触到它。

本节中,我们介绍了0/1背包、完全背包、多重背包和分组背包。除了以传统的线性DP求解之外,我们还尽量缩减了空间复杂度,省去了“阶段”的存储,用适当的循环顺序控制状态在原有基础上直接转移和累积。

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

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

相关文章

乐理基础-节拍器与使用

在 乐理基础-情绪与速度、具体的速度、BPM-CSDN博客 与 乐理基础-抽象的速度-CSDN博客里写了音乐中的速度是怎样去确定的&#xff0c;接下来要写的内容必须要知道怎样去确定音乐的速度才可以。 首先音乐的速度之前用秒来说&#xff0c;是为了方便理解&#xff0c;比如138bpm它…

DC-9靶机

目录 DC-9靶场链接&#xff1a; 首先进行主机发现&#xff1a; sqlmap注入&#xff1a; 文件包含&#xff1a; 端口敲门规则&#xff1a; hydra爆破&#xff1a; root提权&#xff1a; 方法一/etc/passwd&#xff1a; ​编辑 方法二定时任务crontab&#xff1a; DC-9靶…

罗德与施瓦茨FSV40-N手持式频谱分析仪

描述 R&S FSV是速度最快、功能最全面的信号和频谱分析仪&#xff0c;适用于从事RF系统开发、生产、安装和服务的注重性能、注重成本的用户。 频率范围高达3.6 GHz/7 GHz/13.6 GHz/30 GHz 40 MHz分析带宽 0.4 dB级测量不确定度&#xff0c;最高7 GHz 针对GSM/EDGE、WCDMA/…

模块与包、反序列化校验源码分析、断言、drf之请求、drf之响应

模块与包 什么是模块&#xff1f; 一个py文件&#xff0c;被别的py文件导入使用&#xff0c;它就是模块 如果py文件&#xff0c;直接右键运行&#xff0c;它叫脚本文件 什么是包&#xff1f; 一个文件夹&#xff0c;下有 __init__.py &#xff0c;和很多py文件&#xff0c;这个…

MySQL 分表真的能提高查询效率?

背景 首先我们以InnoDB引擎&#xff0c;BTree 3层为例。我们需要先了解几个知识点&#xff1a;页的概念、InnoDB数据的读取方式、什么是树搜索&#xff1f;、一次查询花费的I/O次数&#xff0c;跨页查询。 页的概念 索引树的页&#xff08;page&#xff09;是指存储索引数据…

第十二章 异常-Exception

一、异常的概念&#xff08;P444&#xff09; Java 语言中&#xff0c;将程序执行中发生的不正常情况称为“异常”。&#xff08;开发过程中的语法错误和逻辑错误不是异常&#xff09; 执行过程中所发生的异常事件可分为两大类 &#xff08;1&#xff09;Error&#xff08;错误…

链接未来:深入理解链表数据结构(二.c语言实现带头双向循环链表)

上篇文章简述讲解了链表的基本概念并且实现了无头单向不循环链表&#xff1a;链接未来&#xff1a;深入理解链表数据结构&#xff08;一.c语言实现无头单向非循环链表&#xff09;-CSDN博客 那今天接着给大家带来带头双向循环链表的实现&#xff1a; 文章目录 一.项目文件规划…

找不到msvcp120dll,无法继续执行代码的解决方法大全

当你尝试启动一个应用程序或游戏&#xff0c;并且遭遇到一个错误信息&#xff0c;告诉你“找不到msvcp120dll,无法继续执行代码”或者收到类似的提示&#xff0c;这说明你的操作系统中缺失了一个关键的动态链接库文件&#xff0c;即 msvcp120.dll。这种情况其实并不罕见&#x…

Android Studio 显示前进后退按钮

在写代码的过程中我们经常需要快速定位到先前或者往后的代码位置&#xff0c;可以使用Alt左右箭头 但是新安装的Android Studio工具栏上是没有显示左右箭头的工具按钮的&#xff0c;需要我们设置将Toolbar显示出来 View-Appearance-Toolbar 勾选即可 显示后

2024年安防行业预测:5G与安防视频监控技术的5大关键趋势

5G技术是一项以前所未有的速度和可靠性提供数据传输的技术&#xff0c;它的出现将极大地促进安防视频监控技术的发展。随着5G技术的快速发展&#xff0c;安防视频监控系统将在多个方面迎来显著的改进和创新。伴随着2023年进入尾声&#xff0c;2024即将到来&#xff0c;那么在20…

JS逆向基础

JS逆向基础 一、什么是JS逆向&#xff1f;二、接口抓包三、逆向分析 一、什么是JS逆向&#xff1f; 我们在网站进行账号登录的时候对网页源进行抓包就会发现我们输入的密码在后台会显示为一串由字母或数字等符号&#xff0c;这就是经过加密呈现的一段加密文字&#xff0c;而分…

学习Python后可以从事哪方面的工作呢?

Python是最适合用来做人工智能是编程语言&#xff0c;人工智能时代来临&#xff0c;Python人才缺口与日俱增&#xff0c;薪资也随之水涨船高&#xff0c;前景广阔。 Python做人工智能的好处&#xff1a;简单高效、优质的文档、强大的AI库、海量的模块&#xff0c;成为研究AI常…