动态规划–基础背包问题
y总yyds!!
01背包
每个物品最多只用 一次
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
-
(i,j)
为从前i
个物品中选,总体积不超过j
的所有选法集合 -
f(i,j)的值
为从前i
个物品中选,总体积不超过j
的所有选法集合中价值最大的数 -
集合划分
-
状态计算公式
f(i,j) =Max( f(i-1 , j) , f(i-1 , j-vi) + wi )
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N][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 = 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]);
}
}
cout<<f[n][m]<<endl;
}
滚动数组优化
由于f[i]
层只用到了f[i-1]
层
f(i,j) =Max( f(i-1 , j) , f(i-1 , j-vi) + wi )
i
层需要i-1
层的数据,j
列需要的也都是小于等于j
的数据
其次最终只需要f(n,v)
的值,之前计算的结果不需要
因此可以不断覆盖之前的结果,降低维度,省内存
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
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[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[m]<<endl;
}
完全背包
每件物品有无限个
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 ii 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
-
(i,j)
为从前i
个物品中选,总体积不超过j
的所有选法集合 -
f(i,j)的值
为从前i
个物品中选,总体积不超过j
的所有选法集合中价值最大的数 -
集合划分
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N][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=0;j<=m;j++)
for(int k=0;k*v[i]<=j;k++)
f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
cout<<f[n][m]<<endl;
return 0;
}
减去一层循环 优化
-
如图所示
f[i,j-v]+w
等同于Max(f[i-1,j-v]+w,f[i-1,j-2v],f[i-1,j-3w]+3w……)
-
故可优化状态计算公式为
f[i,j]=Max(f[i-1][j],f[i,j-v]+w)
-
集合划分称为(第i种物品选0个的所有选法 || 第i个物品先选1个的所有选法)
- 直观上可以理解为(第i个物品先选1个的所有选法)集合包括了所有(第i个物品先选k(k>0)个的所有选法)集合
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N][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=0;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]<<endl;
return 0;
}
配合滚动数组再次优化
根据状态计算公式为f[i,j]=Max(f[i-1][j],f[i,j-v]+w)
可以简化为f[j]=Max(f[j],f[j-v]+w)
这个不同于01背包
,由于计算f[i][j]
时需要第i
层的j-w
列(小于j )的数据
所以每一层要从左往右计算
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
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=v[i];j<=m;j++){
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[m]<<endl;
return 0;
}
多重背包
每个物品有si个,数量不同
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
朴素算法
类似于完全背包问题
-
(i,j)
为从前i
个物品中选,总体积不超过j
的所有选法集合 -
f(i,j)的值
为从前i
个物品中选,总体积不超过j
的所有选法集合中价值最大的数 -
集合划分
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
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=0;j<=m;j++)
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
cout<<f[n][m]<<endl;
return 0;
}
无法模仿完全背包的优化
既然朴素版本类似于完全背包问题,那么优化方式是否类似于完全背包一样呢?
完全背包优化最关键的是f[i,j-v]+w
等同于Max(f[i-1,j-v]+w,f[i-1,j-2v],f[i-1,j-3w]+3w……f[i-1][j-k*v]+k*w) k*v<=j
这个等价。
多重背包问题中此条件不成立
主要原因是k的范围带来的问题
在完全背包内,一种物品的数量是无限的,唯一的约束条件的k*v<=j
而在多重背包内,约束条件有k<=si
与k*v<=j
,这便会导致项与项无法完全对应,故也无法等价了
另外从直觉上的解释
在完全背包中,f[i,j-v]+w
可以理解为至少选一个i种物品的所有选法集合
而在多重背包内f[i,j-v]+w
便无法这么解释,因为f[i,j-v]
这个集合内包含选择了si
个i
种物品的情况,再加wi
就等同于存在选择了si+1
个i
种物品的情况,超出限制。
因此优化需要选择其他思路。
二进制优化方法
在朴素做法中,往往是一个一个的枚举,从0枚举到s。
但可以使用另一种方式进行枚举,使用二进制的方式来考虑,换一个方式思考有n种物品,每种物品有si个的条件。
首先一个关键步骤就是打包,与其一个一个的取,不如打包起来,可以直接拿。
#include<bits/stdc++.h>
using namespace std;
const int N = 12010, M = 2010;
//2000*log2000
int n, m;
int v[N], w[N];
int f[M];
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){
cnt++;
v[cnt]=a*k;//k个物品包在一起的新包裹
w[cnt]=b*k;
s-=k;
k*=2;
}
//如果有空隙,就在补上 (2^[lgs]到s的距离)
if(s>0){
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
n=cnt;//通过01背包算法取出前面打包的包裹
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]);
cout<<f[m]<<endl;
return 0;
}
分组背包
物品分组,每组物品内有若干种,每一组内只能选一种
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号 j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
算法思路于之前多重背包问题的朴素算法较为类似
- 状态计算公式
f(i,j) =Max( f( i-1 , j-k*v[i][k] ) + k*w[i][k])
#include<bits/stdc++.h>
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--)
for(int k=0;k<s[i];k++)
if(v[i][k]<=j)
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
cout<<f[m]<<endl;
return 0;
}