背包问题
- 01背包问题
每件物品最多使用一次
- 完全背包问题
每件物品有无限个
- 多重背包问题
每件物品有指定的数量
- 分组背包问题
每件物品只能从固定的组里选,且每组有且只能选择一件物品
2.01背包问题
- DP
- 状态表示
f[i][j]
—指的是从1~i
中选出总体积小于j
的元素的组合,并且表示的属性
是最大值- 集合
- 所有的选法
- 条件
数目限制
(1~i
)体积限制
(<=j
)
- 属性
max
,min
,数量
— 本题中的属性指的是最大价值max
- 状态计算
集合的划分
- 状态表示
集合的划分
- 将集合不重不漏的划分成更小的部分(
类似于组合数的公式拆解
)使其能够用递归方式进行统计- 本题:01背包问题,考虑到集合
f[i][j]
的本质:从1~i
中选出总体积小于等于j
的元素的组合,并且表示的属性
是最大值,将其拆分为选中元素i
,和没有选中元素i
的两个部分- 其中没有选中元素i的部分即可表示成集合
f[i-1][j]
(即从1~i-1中选择总体积为j的元素并且价值最大)- 选中元素i的部分可以表示为
f[i-1][j-vi]+wi
即将i元素从选法中删除,那么代表最大值的选法应当仍然是最大值的选法,而最后的答案加上i元素的价值即可
- 所以最后f[i][j]表示为两个选法的最大值即
f[i][j]=max(f[i-1][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])
在每次计算时,只用到i-1层的数据,联想到可以用滚动数组
进行优化(滚动数组即一种节约空间的优化方式)
滚动数组优化
如优化斐波那契数列
#include<iostream>
const int N=100;
int q[N];
int main(){
q[0]=1;
q[1]=1;
for(int i=2;i<N;i++)q[i]=q[i-1]+q[i-2];
}
注意到这里的每一项
q[i]
只依赖于前面两项,所以转化为滚动数组
#include<iostream>
const int N=100;
int q[3];
int main(){
q[1]=1;
q[2]=1;
for(int i=2;i<N;i++){
q[0]=q[1];
q[1]=q[2];
q[2]=q[0]+q[1];
}
cout<<q[2]<<endl; //输出第N项
}
- 同理,由于每一次只用到上一行的数组值,这里可以优化成一位状态方程
f[j] = max(f[j], f[j-v[i]] + w[i])
- 注意,状态方程
f[i][j]
的值只取决于上一层的f[i-1][j-v[i]]和f[i-1][j]
,如果运用滚动数组并且顺序更新则会出现冲突,如f[i][j-v[i]]
取决于f[i-1][j-v[i]]和f[i-1][j-v[i]-v[i-1]]
而f[i][j]
的值取决于f[i-1][j-v[i]]和f[i-1][j]
如果先将f[i-1][j-v[i]]
更新成f[i][j-v[i]]
,那么f[i][j]
将无法更新
二维数组 | 0 | 1 | … | j-1 | j |
---|---|---|---|---|---|
0 | … | ||||
1 | … | ||||
… | … | ||||
i-1 | … | … | f[i-1][j-v[i]] | … | f[i-1][j] |
i | … | … | f[i][j-v[i]] | … | f[i][j] |
DP+二维数组
#include<iostream>
using namespace std;
const int N=1010;
int v[N],w[N]; //体积,价值
int n,m; //数量,体积
int f[N][N]; //f[i][j]
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
for(int i=1;i<=n;i++) //f[0][1~m]的值默认是0
for(int j=1;j<=m;j++){ //体积从1开始统计到m
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]); //如果j体积大于等于v[i],(i元素可以被选择)取两者最大值
}
cout<<f[n][m];
}
一维数组 | 0 | 1 | … | j-1 | j |
---|---|---|---|---|---|
i-1 (实际不存在这一行) | … | … | f[j-v[i]] | … | f[j] |
i (i的状态取决于i-1的状态) | … | … | f[j-v[i]] | … | f[j] |
DP+一维数组
#include<iostream>
using namespace std;
const int N=1010;
int v[N],w[N]; //体积,价值
int n,m; //数量,体积
int f[N];
int main(){
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[],枚举到体积大于等于v[i]
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[m];
}
3.完全背包问题
- 状态表示与01背包问题相同
f[i][j]
同样表示从1~i元素中选出的元素总体积为j的组合,且为最大值- 不同在于
状态方程的计算
- 同样将其拆解为
有没有选择i
元素的若干部分,由于这里的i虽然有无限多个,但是由于j的束缚也只可以选择有限次,我们假定最多选取n个
- 那么集合
可以划分为
:选择0,1,2,…k,k+1,…,n个i- 状态方程可以表示为
f[i][j]=max(f[i-1][j-k*v[i]]+w[i]*k)k=0,1,...,n-1,n
优化
- 由于状态方程要进行k重的枚举,时间复杂度接近109 所以我们对状态方程进行优化
- 观察状态方程
f[i,j]=max(f[i-1,j],f[i-1,j-v[i]]+w[i],f[i-1,j-2*vi]+2*wi),...f[i-1,j-k*vi]+wi*k.....)
- 将
f[i][j-v[i]]
将其按照同样计算的方式展开,得:f[i,j-vi]=max(f[i-1,j-vi],f[i-1,j-2*vi]+wi,f[i-1,j-3*vi]+2*wi....f[i-1,j-k*vi]+wi*(k-1)...
- 对比两个多项式可以发现
f[i][j-vi]
的展开式是f[i][j]
的展开式从第二项开始每一项增加一个wi
由此,可以将f[i][j]
的展开式简化为f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i])
DP+二维
#include<iostream>
using namespace std;
const int N=1010;
int f[N][N];
int n,m;
int v[N],w[N];
int main(){
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=1;j<=m;j++){
f[i][j]=f[i-1][j];
if(j>=v[i])f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
cout<<f[n][m];
}
优化成一维
:由于不再需要取决于上一层的状态f[i-1][j-v[i]]
按照顺序计算即可
DP+一维
#include<iostream>
using namespace std;
const int N=1010;
int f[N];
int n,m;
int v[N],w[N];
int main(){
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=v[i];j<=m;j++)
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m];
}
4.多重背包问题
- 朴素写法与完全背包问题相同,只不过此时枚举的k为给定的有限值
s[i]
- 状态方程可以表示为
f[i][j]=max(f[i-1][j-k*v[i]]+w[i]*k)k=0,1,...,s[i]
- 此时时间复杂度为
O(n*v*s)
接近106 所以不超时
#include<iostream>
using namespace std;
const int N=110;
int n,m;
int v[N],w[N],s[N];
int f[N][N];
int main(){
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=1;j<=m;j++)
for(int k=0;k<=s[i];k++)
if(j>=k*v[i])f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]); //枚举每一个可能的k的数量
cout<<f[n][m];
}
5.多重背包问题II
- 由于数据范围为 1000 ∗ 2000 ∗ 2000 = 4 ∗ 1 0 9 1000*2000*2000=4*10^9 1000∗2000∗2000=4∗109 所以采用三重循环会超时
- 若采用与完全背包问题一样的思路优化,不难看出,难以求出后项的最大值
- 观察数量:一共有N*S件物品
- 用二进制数去打包N*S件物品,将其打包成新的组合,并且每个组合的数量只有一个
- 例如:若s=1023,则我们不需要枚举从
j-v,j-2v....j-sv
这1023种情况- 由于二进制
1 000 000 000
,十位的二进制可以表示从0~1023的范围,他的每一位相当于打包成了1,2,4,8...512
组合的物品,结果我们就把原来的N*S件
物品分成了N *logS件
物品,- 由于新的N*logS件物品每样只有一个,对这些物品
做一遍01背包问题
即可
- 倘若s的范围并不在一个恰好的二进制数内,例如s=200
- 可以将s打包成
1,2,4....64
这些新的组合数量的物品,即0 000 000 ~ 1 111 111
0~127这个范围,并且用一个c件物品,c=200-127=73
来填补这个空缺- 此处由于二进制最高位
k=7
,并且c<2^k+1
,所以填补的范围不会有空缺
- 这样一来时间复杂度被降低为 O ( n ∗ v ∗ l o g s ) = 1000 ∗ 2000 ∗ l o g ( 2000 ) = 2 ∗ 1 0 7 O(n * v *logs) =1000*2000*log(2000)=2*10^7 O(n∗v∗logs)=1000∗2000∗log(2000)=2∗107
log2000~=11
#include<iostream>
using namespace std;
const int N=25000,M=2010; //N=n*logs~=2000*11=22000个元素,这里开到25000
int n,m;
int v[N],w[N];
int f[M]; //一维01背包问题
int main(){
cin>>n>>m;
int cnt=0;
for(int i=1;i<=n;i++){
int a,b,s;
cin>>a>>b>>s; //输入每一件物品的体积,价值,数量
int k=1;
while(k<s){ //将s分类打包成新的logs组物品
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
if(s>0){ //打包多余的零头
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
for(int i=1;i<=cnt;i++) //对于所有的N*logS,做一遍01背包问题
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m];
}
9.分组背包问题
- 状态表示与01背包问题相同
f[i][j]
表示从1~i元素中选出的元素总体积为j的组合,且为最大值- 集合划分:
- 由于每一组物品最多只能选一个或者不选,所以我们考虑
f[i][j]
的状态方程为选不选第i组当中的某一个物品
- 不选:
f[i][j]=f[i-1][j]
- 选
f[i][j]=f[i-1][j-v[i,k]]+w[i,k],k=1,2,3,.....s[i]
f[i][j]=max(f[i-1][j],f[i-1][j-v[i,k]]+w[i,k])
#include<iostream>
using namespace std;
const int N=110;
int n,m;
int v[N][N],w[N][N],s[N];
int f[N];
int main(){
cin>>n>>m;
//初始化每一组元素
for(int i=1;i<=n;i++){
cin>>s[i];
for(int j=0;j<s[i];j++)
cin>>v[i][j]>>w[i][j]; //输入每一组中每一个元素的体积,价值
}
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--) //与01背包优化相同,因为需要依赖于上一组的值,所以倒序枚举
for(int k=0;k<s[i];k++) //枚举每一组所有可能选的元素
if(j>=v[i][k])f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
cout<<f[m];
}