01背包及其优化
题意
有n个物品,在容量不超过m并且每件物品只能选择拿或者不拿,第i件物品有自己的价值w[i]与体积v[i]。
状态转移方程
f[i][j] 表示前i件物品在体积不大于j的情况下的最大价值。
(1)不选择第i件物品,f[i][j] = f[i - 1][j],即在有i-1件物品并且容量为j的情况下的最大值。
(2)选择第i件物品,f[i][j] = f[i-1][j - v[i]] + w[i]。
所以状态转移方程
f[i][j] = max(f[i - 1][j],f[i-1][j - v[i]] + w[i]);
代码
for(int i = 1;i<=n;i++){
for(int j = 1;j <= m;j++){
f[i][j] = f[i-1][j];
if(v[i] <= j){
f[i][j] = max(f[i][j],f[i-1][j-v[i]] + w[i]);
}
}
}
复杂度O(n × m)
优化
f数组一维降二维
为什么可以将二维降到一维呢?因为我们只关心在每个物品选或者不选的情况下容积为m时的最大价值,并不在意你具体选了多少物品。
先上代码
int main(){
int n,m;
cin>>n>>m;
for(int i = 1;i <= n;i++){
cin>>v[i]>>w[i];
}
//二维到一维优化,
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]);
}
}
//为什么f[m]就是容积<m的情况下的最大价值
cout<<f[m];
return 0;
}
在降到一维之后,f[j]代表容积为j时的最大价值。
需要注意的问题
1、注意上述代码的第9行,for循环中是降序进行循环的,在二维中我们知道f[i][j] 是依赖于 f[i-1][j]的,在一维中一样如此,在降维过程中,我们只是简单的把f[i][j]中的i简单的抹除掉了,再来看上述问题,如果正序循环,在第i轮循环应该使用f[i-1][j]进行更新,但因为正序循环此时使用的是f[i][j]进行更新,举个例子,若第i轮对体积为3的物体进行决策,一维中,f[6] = f[4],但因为从小到大更新,本应该为f[i-1][4]变为了f[i][4],如果是逆序遍历,计算f[6]时f[4]还没在当前这一轮被计算,f[4]取f[i-1][4]。
2、为什么f[m]就是容积为m的前提下的最大价值。
先给出结论:当f[n]初始化全为0时,f[m]代表容积<=m时的最大价值;仅当f[0]初始化为0,其他为-inf时,f[m]代 表体积恰好是j的最大值。
这里我们可以反向思考,当f[m]表示容积<=m时的最大价值,dp的边界是f[0][j]表示在容积为j时选择0件物品的最大值,这么选择是合法的,但如果要这么选必须将f[0][j]初始化为0,因为咱们一件物品也没有选。当f[m]表示恰好代表容积为j时的最大值,此时f[0][j]表示容积为j并且一件也没选时的最大值,那么只有当j==0时有合法方案,所以f[0][0] = 0,其他为-inf。
完全背包
相比于01背包,完全背包要求每件物品可以选择无限次。
根据上图状态方程可得知(3)式即为完全背包的状态转移方程。与01背包中的状态转移方程比较一下
f[i][j] = max(f[i-1][j],f[i-1][j-v[i]] + w[i]) //01
f[i][j] = max(f[i-1][j],f[i][j-v[i]] + w[i]) //完全
可以看出只有下标i不同,此下表代表是否是当前轮次的值,所以完全背包问题可以去掉在01背包问题中逆向循环的操作,从而改为正序循环。
//written by 2023/6/13
#include<iostream>
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[N],w[N];
int f[N];
int main(){
int n,m;
cin>>n>>m;
for(int i = 0;i < n;i++){
cin>>v[i]>>w[i];
}
for(int i = 0;i < n;i++){
for(int j = v[i];j <= m;j++){
f[j] = max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[m];
return 0;
}
多重背包
多重背包要求物品共有有限件,即最多选s件。
方法一
可以将物品i共有s件看为有s件物品i,这s件物品可选可不选,就可以看成01背包问题。
代码就不放啦
方法二
二进制优化,即无论s为多少都可以由1、2、4、8....与s-(1+2+4+8+...)组成,那么即可根据每件物品的数量来求出这样一个数组,该数组中的元素组合可以表示任何<=s的值。
#include<iostream>
#include<bits/stdc++.h>
using namespace std;
const int N = 2010;
struct Good{
int v,w;
};
int f[N];
int main(){
int n,m;
cin>>n>>m;
vector<Good> Goods;
for(int i = 0;i < n;i++){
int v,w,s;
cin>>v>>w>>s;
for(int k = 1;k <= s;k = k * 2){
s -= k;
Goods.push_back({k * v,k * w});
}
if(s > 0) Goods.push_back({s * v,s * w});
}
for(auto good : Goods){
for(int j = m;j >= good.v;j--){
f[j] = max(f[j],f[j - good.v] + good.w);
}
}
cout<<f[m];
return 0;
}
分组背包
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是v[i][j]价值是w[i][j],其中i是组号,j是组内编号。
分组背包没有什么好的优化方法,解法即是在01背包基础上,在选择物品时在所在的分组中选择一个,多加了层循环。同时也保证了每个分组中只选择了一个,即对于f[j]要选择物品时在目前所在分组中选择一个物品使f[j]最大。
#include<iostream>
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int v[N],w[N];
int f[N];
int main(){
int n,m;
cin>>n>>m;
for(int i = 0;i < n;i++){
int s;
cin>>s;
for(int j = 1;j <= s;j++) cin>>v[j]>>w[j];
for(int j = m;j >= 0;j--){
for(int k = 1;k <= s;k++)
if(j >= v[k])
f[j] = max(f[j],f[j - v[k]] + w[k]);
}
}
cout<<f[m];;
return 0;
}
写在最后,上述只是作者自己的一些理解,如有错误之处,还希望大家都多指出。