背包问题
目录
-
01背包
-
完全背包
-
多重背包
-
混合背包
-
二维费用背包
-
附录 二进制拆分的简单证明*
-
附录 习题
一、01背包
01背包,意思就是要么是0要么是1,即每种物品仅有一件。
有
Part 1 O(VN) 算法1
通过简单分析,可以得出
无法一次求解,需要递推(动态规划)
显然,按物品的数量顺序递推(指“
1 件物品最大价值”\Rightarrow “2 件物品最大价值”\Rightarrow ……\Rightarrow “n 件物品最大价值”的递推顺序)明显比按重量大小顺序递推(指“重量为1 时的最大价值”\Rightarrow “重量为2 时的最大价值”\Rightarrow ……\Rightarrow “重量为n 时的最大价值”)更快、更简单,则考虑按物品的数量顺序由小到大作为主循环对于第
i 件物品,有两种选择:选;不选
则能得出解法:
状态
转移方程及过程
即 f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+w[i])
解析:对于“从第
i-1 件物品的选择转化到第i 件物品的选择”有两种情况:
在选了
i-1 件物品的基础上,选择第i 件物品,当前背包承重减去c_i ,当前价值加上w_i 在选了
i-1 件物品的基础上,无所事事(跳过第i 件物品的选择,即不选择第i 件物品)取最大值即可
得到转移方程,考虑过程为
for i=1..N
for j=c[i]..V
f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+w[i]);
for(int i=1;i<=n;i++){
for(int j=c[i];j<=V;j++){
f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+w[i]);
}
}
解析:
第一层循环表示当前物品数量,循环顺序显然,不做解释
第二层循环表示重量,循环顺序无所谓,正反皆可(注意范围为
c_i\sim V )
Part 2 O(VN) 算法2
考虑:对于每次转移,我们仅仅只需要
状态、转移方程及过程
即 f[j]=max(f[j],f[j-c[i]]+w[i])
可得过程
for i=1..N
for j=V..c[i]
f[j]=max(f[j],f[j-c[i]]+w[i]);
for(int i=1;i<=n;i++){
for(int j=V;j>=c[i];j--){
f[j]=max(f[j],f[j-c[i]]+w[i]);
}
}
解析:由于第二层循环(重量)是倒着来的,所以在第
i 层时j 还没跑到j-c_i 的时候,f_{j-c_i} 其实是在第i-1 层重量为j-c_i 的最大价值(因为第i 层重量为j-c_i 还没跑过)。这就满足了我们之前二维数组时的需求:判断f_{i,j} 要看f_{i-1,j} 和f_{i-1,j-c_i} (因为重量是倒着跑的,所以在第i 层重量为j 的f 数组还未被赋值时,当前的f_j 和f_j-c_i 其实都是在第i-1 层时的值)。所以,第二层循环仅能倒着跑
Part 3 一些有用的优化&技巧
循环次数1
前面已经给出了,我们将第二层循环的 V..0 转化为 V..c[i],即可将
循环次数2
注意,在背包九讲中,原作者将这里的代码和内容写错了。。。
该优化可以使
考虑:由于最终我们仅需要知道 f[V-c[n]-c[n-1]]) 即可……以此类推,在
得出代码(由于这里要用到后缀和,所以只写伪代码)
for i=1..N
for j=V..max(V-sum(c[i+1..n]),c[i])
f[j]=max(f[j],f[j-c[i]]+w[i]);
初始化
一般的题目可以分为两种:必须装满;可以不装满
考虑将无解的情况输出
那么,对于必须装满的情况,我们只需要将所有除去
对于不必须填满的情况,我们看作是不论重量大小都可以做为解,所以要将所有位置都初始化为
Part 4 小结
01背包是最最最最基础的一种背包问题了,后面的大多数背包问题都是建立在01背包的基础上的
二、完全背包
有
分析:
- 性质:
每件物品有无数件
物品的选择没有顺序
Part 1 O(VN\sum \frac{V}{c_i}) 算法
由于害怕误导,不做代码展示和过多解释,仅做简单解释。
在01背包的基础上,枚举
状态+转移方程
即f[i][j]=max{f[i-1][j-k*c[i]]+k*w[i]},(0<=k*c[i]<=j)
显然,时间复杂度在大部分题目中并不允许,考虑优化
Part 2 转化为01背包的算法(一)
该方法在时间上较Part 1的算法有负优化,在空间上较Part 1的算法可能有负优化,所以同样不做展示和过多解释
考虑第
Part 3 转化为01背包的算法(二)
二进制拆分
该算法较Part 1&Part 2的算法有些许优化,使用了更高效的转化方法,但仍然不满足正常题目的需求,所以也不做展示和过多的解释
考虑对于第
关于具体的证明可以到 附录 二进制拆分的简单证明 查看
Part 4 O(VN) 算法
该算法是在01背包算法的基础上建立的
状态
转移方程及过程
与01背包并无太大区别,仅仅只相差一个循环顺序
即 f[j]=max(f[j],f[j-c[i]]+w[i])
for i=1..N
for j=c[i]..V
f[j]=max(f[j],f[j-c[i]]+w[i]);
解析:
- 在第
i 次循环,重量为j 时,f_j 的值取决于f_j 和f_{j-c_i}+w_i 。思考一下,取值中的f_j 应该是第i 次循环中的值还是第i-1 次循环中的值?如果是在01背包中,显然答案是后者。但在完全背包中,一个物品可以选无数次。而这里正着跑时,j-c_i 显然已经被算过了。这样就完成了完全背包的特性。所以,重量的循环只能正着跑
在背包九讲中,还提到了“上面的伪代码中两层for循环的次序可以颠倒”“这个结论有可能会带来算法时间常数上的优化”。本文不做解释和介绍。
Part 4 优化技巧
去重
存在
方法
可以选择用
三、多重背包
与完全背包很像,很多思考过程也与完全背包相似
有
Part 1 O(VN\sum n_i) 算法
与完全背包类似,错误做法同样不做过多解释和展示。
状态+转移方程
即f[i][j]=max{f[i-1][j-k*c[i]]+k*w[i]},(0<=k*c[i]<=min(n[i],V/c[i]))
Part 2 转化为01背包的算法(一)
与完全背包类似,同样不做过多解释和展示
考虑对于第
Part 3 转化为01背包的算法(二)
二进制拆分
考虑对于第
关于具体的证明可以到 附录 二进制拆分的简单证明 查看
Part 4 O(VN) 算法
注意,在某些题目中,该方法常数较大,有可能不如二进制拆分快。但在大部分情况下还是该算法更快一些的。
单调队列
思路+状态转方程
若当前重量为
回看Part 1中的方程(这里我省略着写的)
其中,
在后面,我会用
我们将刚刚得出的量代进去:
得
令
因为
可得出
方程就变成了
当然,对于第
现在,问题转化成了:
而我们可以得出共有
考虑使用单调队列,用
当然,考虑对于第
显然重量为
考虑分组后,我们按组来做即可。
则总时间复杂度为
代码实现
见P1776
int c,w,num;
if(c==0){
ans+=w*num;
continue;
}int l=min(num,V/c);
for(int modse=0;modse<c;modse++){
int t=(V-modse)/c;
head=1,tail=0;
for(int k=0;k<=t;k++){
int New=f[k*c+modse]-k*w;
while(head<=tail&&New>=q[tail].w)tail--;
tail++;
q[tail].id=k,q[tail].w=New;
while(head<=tail&&q[head].id+l<k)head++;
f[k*c+modse]=q[head].w+k*w;
tmp=max(tmp,f[k*c+modse]);
}
}
由于代码太麻烦,就不给出伪代码了
文中 modse 表示模数,head & tail 表示队列的头和尾的位置,ans 表示不占体积的物品的价值总值,tmp 表示所有物品中解的最大值(不计算不占体积的物品的价值)。注意,代码中给出的是处理一件物品的过程
四、混合背包
这里的混合背包指的是混合01背包、完全背包、多重背包
有
-
1\le n_i\le \infty
Part 1 引导
考虑在前三种背包中,我们通常将每件物品分着来做(将每件物品分别考虑方程)。而且,不同物品的方程不同,不会影响到结果
后面的两个部分是证明“不同物品的方程不同,不会影响到结果”这句话的,如果您已经理解,可以直接转到Part 2。
我们可以将完全背包和多重背包转化为01背包(虽然实际上我们的解法不同,但在理论上它们是可以转化的)
证明:由于对于第
我们将其转化为
所以,调换方程不影响结果
Part 2 算法
算法时间复杂度视个人想法来定
有了Part 1的证明,我们只需要对每件物品分别做方程即可。当然,每件物品你可以选择时间复杂度不同的方程。
伪代码:
for i=1..N
if 物品i为01背包
01背包的方程...
if 物品i为完全背包
完全背包的方程...
if 物品i为多重背包
多重背包的方程...
条件符合的情况下,时间复杂度可以达到
五、二维费用背包
二维费用背包即每个物品有两种费用,当你选择这件物品时,两种费用要同时花费。
有
注意,后面的一些背包问题不再考虑每件物品的数量,因为他们都可以与前文中的背包问题相对应。出于方便,我们这里考虑的是01背包的做法。
这种问题分布其实比较普遍。某些背包问题表面上只有一种费用,但题目中要求最多只能选
Part 1 复数域上的背包问题(不做详解)
在背包九讲中有提到
我们可以将一件物品转化为复数的形式,那么问题就转化成了复数域上的背包问题了。
Part 2 O(ABN) 算法
状态
转移方程及过程
根据前面的经验,我们可以轻松得出转移:
过程也同样简单,只给出伪代码
for i=1..N
for u=1..A
for v=1..B
f[i][u][v]=max(f[i][u][v],f[i][u-a[i]][v-b[i]])
这里第二维循环和第三维循环顺序可以随便交换,不影响
简单优化
因为是01背包,状态第一维可以滚掉(将第二维和第三维的循环顺序反过来,即从小到大变为从大到小)。即便是完全背包和多重背包,仍然可以利用前面讲的方式优化。
附录 二进制拆分的简单证明
以后有空再写
附录 习题
echo的背包题单
01背包
P1048 [NOIP2005 普及组] 采药
P1060 [NOIP2006 普及组] 开心的金明