3.多重背包问题
1.题意
有N 种物品和一个容量为V 的背包。第i 种物品最多有 s[i] 件可用,每件体积是v[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
2.思路
这题目和完全背包问题很类似。因为对于第i 种物品有s[i]+1种策略:取0 件,取1 件……取p [ i ] 件。令dp [ i ] [ j ] 表示前i 种物品恰放入一个容量为j 的背包的最大价值,则有状态转移方程:
dp[i] [j]=max(dp[i−1] [j−k∗v[i]]+k∗w[i])|0≤k≤s[i]
#include<bits/stdc++.h>
using namespace std;
int v[105],w[105],s[105];
int dp[105];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i]>>s[i];
}
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
for(int k=1;k<=s[i]&&k*v[i]<=j;k++){
dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);
}
}
}
cout<<dp[m];
}
但是这样写的话时间复杂度会很高O(n*m *s[i]),如果数据为2000就会超时,所以要进行优化。
3.代码优化
在进行优化前先讲一个小例子:
假设有 50个苹果,现在要取n个苹果,朴素的做法是将苹果一个一个拿出来,直到取出n个。再假设有 50个苹果和 6个箱子,利用箱子进行一些预备工作,可以在每个箱子中放 2^k个苹果。也就是1,2,4,8,16,19(剩余的数),取任意n个苹果时,只要推出几只箱子就可以了,例如n=3,我们只需推出前两个物品。可以证明对于任意的n,这种组合方式总是存在的,是不是更加的简便。
二进制拆分思想:将第 i种物品拆分成若干件物品,每件物品的体积和价值乘以一个拆分系数(1,21,22…2^(k-1),s[i] -2^k+1(剩余的数)),就可以转化为 01背包的物品求解。例如,s[i] =12,拆分系数为1,2,4,5,转化为 4件 01背包的物品:(v[i],w[i]),(2v[i],2w[i]),(4v[i],4w[i]),(5v[i],5w[i])。所以各组相互组合包含了0~s[i]的所有情况。
#include<bits/stdc++.h>
using namespace std;
const int N=25000; //2的12次方大于2000,也就是一个数可以拆分12个
int vv[N],ww[N],ss[N]; //所以数组容量乘以12
int dp[2001];
int main(){
int n,m;
cin>>n>>m;
int w,v,s,t=0;
for(int i=1;i<=n;i++){
cin>>v>>w>>s;
for(int j=1;j<=s;j<<=1){ //二进制拆分
vv[++t]=j*v; //存体积
ww[t]=j*w; //存容量
s-=j; //物品件数减去拆分的
}
if(s){
vv[++t]=s*v; //如果数量还有剩余
ww[t]=s*w;
}
}
for(int i=1;i<=t;i++){ // 01背包
for(int j=m;j>=vv[i];j--){
dp[j]=max(dp[j],dp[j-vv[i]]+ww[i]);
}
}
cout<<dp[m];
}
方法二:
int num = min(s, m / v); // m/v是最多可以放几个,优化上界
for (int k = 1; num > 0; k <<= 1) { //这里的k就相当于上面例子中的1,2,4,6
if (k > num) k = num;
num -= k;
for (int j = m; j >= v * k; j--) // 01背包
dp[j] = max(dp[j], dp[j - v * k] + w * k);
}
优化后的代码可以解决多重背包2,下面有b站的讲解视频,供大家查阅
4.混合背包
1.题意
如果将前面三个背包混合起来,也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包),应该怎么求解呢?
2.思路
我们可以对不同的背包用不同的方法计算,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是Θ ( V ∗ N ) ,最后再加上多重背包,多重背包要用二进制优化,不然会超时。在做多重背包的问题时尽量用二进制优化。
#include<bits/stdc++.h>
using namespace std;
int dp[1005];
int main(){
int n,m;
int v,w,s;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v>>w>>s;
if(s==0){
for(int j=v;j<=m;j++)
dp[j]=max(dp[j],dp[j-v]+w);
}
else if(s==-1){
for(int j=m;j>=v;j--)
dp[j]=max(dp[j],dp[j-v]+w);
}
else{
int num=min(s,m/v);
for(int k=1;num>0;k<<=1){
if(k>num) k=num;
num-=k;
for(int j=m;j>=v*k;j--)
dp[j]=max(dp[j],dp[j-v*k]+w*k);
}
}
}
cout<<dp[m];
}
5.二维费用背包
1.题目
二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设第i 件物品所需的两种代价分别为c [ i ] 和g [ i ] 。两种代价可付出的最大值(两种背包容量)分别为V 和M。物品的价值为w[i]。
2.思路
和 01背包一样,费用加了一维,只需状态也加一维即可。设dp[i ] [j ] [k] 表示前i 件物品付出两种代价分别最大为j 和k 时可获得的最大价值。状态转移方程就是:
dp[i] [j] [k]=max(dp[i−1] [j] [k],dp[i−1] [j−v] [k−m]+w])
#include<bits/stdc++.h>
using namespace std;
int dp[1005][105][105];
int main(){
int m1,n,v1;
cin>>n>>v1>>m1;
int v,w,m;
for(int i=1;i<=n;i++){
cin>>v>>m>>w;
for(int j=1;j<=v1;j++){
for(int k=1;k<=m1;k++){
dp[i][j][k]=dp[i-1][j][k];
if(j>=v&&k>=m)
dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-v][k-m]+w);
}
}
}
cout<<dp[n][v1][m1];
}
如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量j 和k 采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时可拆分物品。最终答案为dp [ V ] [ M ],代码:
#include<bits/stdc++.h>
using namespace std;
int dp[105][105];
int main(){
int m1,n,v1;
cin>>n>>v1>>m1;
int v,w,m;
for(int i=1;i<=n;i++){
cin>>v>>m>>w;
for(int j=v1;j>=v;j--){
for(int k=m1;k>=m;k--){
dp[j][k]=max(dp[j][k],dp[j-v][k-m]+w);
}
}
}
cout<<dp[v1][m1];
}
物品总个数的限制
有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M 件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1 ,可以付出的最大件数费用为M 。换句话说,设dp [ i ] [ j ] 表示付出费用i 、最多选j 件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新。
6.分组背包问题
1.题意
有N 件物品和一个容量为V 的背包。第i 件物品的体积是v[ i ] ,价值是w [ i ] 。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
2.思路
和 01背包类似,只不过进行了分组,那么对于每一组的物品有若干种策略:是选择本组的某一件,还是一件都不选。
也就是说明dp [ k ] [ j ] [j]表示前k 组物品花费费用j 能取得的最大价值,则有:
dp [k] [j]=max(dp[k−1] [j],dp[k−1] [j−v[i]]+w[i]|物品i⊆组k)
#include<bits/stdc++.h>
using namespace std;
int dp[10005],v[105],w[105];
int main(){
int m,n;
cin>>n>>m;
int s;
for(int i=1;i<=n;i++){
cin>>s;
for(int j=1;j<=s;j++){
cin>>v[j]>>w[j];
} // 01背包体积从后往前遍历,防止答案被覆盖
for(int j=m;j>=0;j--) //因为每组物品只能选一个,
for(int k=1;k<=s;k++){ //所以可以覆盖之前组内物品最优解的来取最大值
if(j>=v[k])
dp[j]=max(dp[j],dp[j-v[k]]+w[k]);
}
}
cout<<dp[m];
}
与多重背包的关系
分组背包在面对一组内s ss个的物品时,共有s + 1 种决策情况,分别是选第0 , 1 , 2… s 个物品(选第0个物品即不选该组内任何物品)。多重背包中每个物品有s 个,也有s + 1种决策情况,分别是选0 , 1 , 2… s 个该物品,在此可以和上面的情况做一下对比区分。多重背包可以说是分组背包的一个特殊情况,所以多重背包可以用放弃数组完整性的代价来优化算法。
小结
分组的背包问题将彼此互斥的若干物品称为一个组,这建立了一个很好的模型。不少背包问题的变形都可以转化为分组的背包问题(例如有依赖的背包),由分组的背包问题进一步可定义“泛化物品”的概念,十分有利于解题。
7.背包问题求方案数
1.题意
和 01背包一样,就是求当背包达到最优解时,有几种不同的选择方案。
2.思路
我们可以用两个数组一个数组dp[i] 代表前i件物品恰放入一个容量为j的背包可以获得的最大价值,另一个数组c[i]代表前i 件物品体积为j 时,价值最大的方案数。
1.如果第 i个物品我们可以选择,且总价值增大,则更新do[j]和c[j]。**背包容量从 j-v更新到 j,只是多装入一个物品,没有改变方案数,所以c[j]=c[j-v]。**状态就由c[j] 转移到了c[j-v]。
2.若第 i个物品装入背包,且总价值没有增加,那么总价值就不用再更新了,对于方案数c 那就有两种情况:
-
若不装入该物品,容量已有的方案数为c[j];
-
若装入该物品,容量 j对应的方案数为c[j-v]; 我们放入该物品时的价值和不放入该物品时的价值相同说明这是两种 不同的情况,对于这两种情况我们我们求和,就是当体积为j 时,价值最大的方案数。
3.如果不装入第 i个物品,那么容量 j 已有的方案数为 c[j]。
下面的 f 数组代表dp数组,左边有 >代表是选当前物品价值大于不选的时候,= 就是相等的时候。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll mod=1e9+7;
ll dp[1005],c[1005]; //最大价值,方案数
int main(){
int n,m;
cin>>n>>m;
int v,w;
for(int i=0;i<=m;i++)
c[i]=1;
for(int i=1;i<=n;i++){
cin>>v>>w;
for(int j=m;j>=v;j--){
if(dp[j-v]+w>dp[j]){ //如果选当前物品比不选的价值更大的话,那么就更新dp[j]的值
dp[j]=dp[j-v]+w; //更新方案数为c[j-v]
c[j]=c[j-v]%mod;
}
else if(dp[j]==dp[j-v]+w) //如果选和不选的价值相等的话,那么这就是两种情况
c[j]=(c[j]+c[j-v])%mod; //两种情况的方案数相加
}
}
cout<<c[m];
}
8.背包问题求具体方案数
1.题意
和求方案数的题一样,只不过这个要输出具体的选择方案,输出字典序最小的。
2.思路
假设存在包含第1 个物品的最优解,为了确保字典序最小那么我们必然要选第一个。那么问题就为从2~n这些物品中找到最优解。
首先我们从后向前遍历物品,让最优解落在dp[1] [m]中。
状态定义:dp[i] [j]代表从第i 个物品到最后一个物品装入容量为j 的背包最大价值(01背包为前i 个,这个是从第i 个到第n 个)
状态转移:dp[i] [j]=max(dp[i+1] [j],dp[i+1] [j-v[i]]+w[i])
不选第i 个物品:dp[i] [j]=dp[i+1] [j]
选第i 个物品:dp[i] [j]=dp[i+1] [j-v[i]]+w[i]
下面的 f 数组就代表dp 数组
#include<bits/stdc++.h>
using namespace std;
int dp[1005][1005];
int v[1005],w[1005];
int main(){
int m,n;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i];
}
for(int i=n;i>=1;i--){ //逆序取物
for(int j=0;j<=m;j++){
dp[i][j]=dp[i+1][j]; //先赋值上一层的值
if(j>=v[i])
dp[i][j]=max(dp[i][j],dp[i+1][j-v[i]]+w[i]);
}
}
int j=m; //剩余容量
for(int i=1;i<=n;i++){ //找路,从1开始是否满足这个等式
if(j>=v[i]&&dp[i][j]==dp[i+1][j-v[i]]+w[i]){
cout<<i<<' '; //如果满足这个等式说明一定选择了这个这个物品,就输出
j-=v[i]; //选了第 i个物品,剩余容量减小
}
}
}
下面有一个选择的过程:
总结
先循环物品再循环体积再循环决策,01背包体积逆序,完全背包体积正序