01背包
题目描述
有一个体积为M的背包,N件物品,每件物品都有对应的体积v[i],价值w[i],将哪几件物品放入背包可使得背包中的物品价值之和最大且不超过背包的体积。
思路
每件物品只有一个,对于每件物品只有放入背包与不放入背包两种选择。
令f[i][j]表示前i个物品放入体积为j的背包的最大价值,最终答案为f[M][N]。
状态转移方程:\(f[i][j]=max(f[i-1][j-v[i]]+w[i],f[i-1][j])\),可以用数学归纳法证明其正确性。
核心代码
for(int i=1;i<=N;++i){//遍历物品
for(int j=0;j<=M;++j){//遍历容量
if(j>=v[i])f[i][j]=max(f[i-1][j-v[i]]+w[i],f[i-1][j]);
else f[i][j]=f[i-1][j];
}
}
空间优化
通过状态转移方程可以看出,f[i]这一层的状态只与f[i-1]这一层有关,那么当i很大时,会保存很多的冗余数据,故可考虑空间优化。
最直观的方法为开两个一维数组,每次只保存相邻的两个状态。
另一种方法为开一个一维数组,更新数组时倒序遍历数组。(注意到f[i][j]只与之前(左边)的状态(f[i][j-v[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]);
完全背包
题目描述
完全背包与01背包的不同之处在于完全背包的每个物品都有无限多个。
思路
由于每件物品有无限个,故每件物品的选择有多种,放入0个到背包,放入1个到背包,放入2个到背包...一直达到背包的体积上限。
令f[i][j]表示前i个物品放入体积为j的背包的最大价值,最终答案为f[M][N]。
状态转移方程:\(f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i],f[i-1][j-2*v[i]]+2*w[i],...f[i-1][j-n*v[i]]+n*w[i])\),\(\quad\) \(n*v[i]\le j\)
优化
将状态转移方程改写:
\(f[i][j]=max(f[i-1][j],\)
\(max(f[i-1][j-v[i]]+w[i],f[i-1][j-2*v[i]]+2*w[i],...f[i-1][j-n*v[i]]+n*w[i]))\),\(\quad\) \(n*v[i]\le j\)
在求最大值时若能将后面那一部分看成整体,这个整体表示的意思时当前物品放1个及以上的最大价值,则转移方程又与01背包有了相同的形式。
先看代码
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]);
可以看到表示容量的那一层循环的遍历方向发生了改变。
假设先在有体积为3的物品J,价值为w,从f[3]=max(f[3-3]+w,f[3])可知
f[3]表示当背包体积为3时,放入0个J与放入1个J的最大价值。
f[6]=max(f[6-3]+w,f[6])=max(f[3]+w,f[6]),由于f[3]表示放0个与1个的最大价值,则f[3]+w表示放1个与2个的最大价值,
括号里的f[6]表示一个都不放,则等式左边的f[6]表示放0个,1个,2个的最大价值。
f[9]=max(f[9-3]+w,f[9])...
经过分析可得代码中的f[j-v[i]]+w[i]表示当前物品放1个及以上的最大价值。
不是整数倍的体积状态分析过程类似。
多重背包
问题描述
有一个体积为M的背包,N种物品,每种物品都有对应的体积v[i],价值w[i]以及个数n[i],将哪几件物品放入背包可使得背包中的物品价值之和最大且不超过背包的体积。
法一 暴力求解\(O(N\sum n[i])\)
转化为01背包求解,即将第i种物品转化为01背包中的n[i]个物品。
法二 二进制优化\(O(N\sum log\ n[i])\)
假如现在有一种商品,其个数为11,按照法一的做法,我们要枚举12次,(放0,1,2,...,12个物品到背包)。
若我们将11个物品打包成4分,每份物品的个数为1,2,4,4,显然1,2,4,4,可以表示0~12中的所有数字,此时只需要枚举4次。
这就是二进制优化,下面介绍下如何打包。
11=0111(B)+0100(B)
将11拆成两部分,第一部分是不超过11的最大的\(2^n-1\),第二部分是剩下的数(如果有的话)。
将\(2^n-1\)的每一位都打包成一份物品(二进制状态下),
例如0111(B)变成0100(B),0010(B),0001(B)。
由二进制的性质可得,这些数能表示0~\(2^n-1\)的所有数。
由于第二部分数比小于等于第一部分数(不然你就拆错了),将第二部分打包,此时所有包裹的组合能表示所有物品数。
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=2e3+5;
int f[N];
struct good{
int v,w;
};
vector<good>a;
int n,v,vi,wi,si,V;
int main(){
cin>>n>>v;
for(int i=0;i<n;++i){
cin>>vi>>wi>>si;
int s=si,k=1;
for(k=1;k<s;k*=2){//读一个拆一个,拆完直接DP
s-=k;//以2的k次方为单位来拆
for(int j=v;j>=k*vi;--j)f[j]=max(f[j-k*vi]+k*wi,f[j]);
//从这里可以看出第二部分数小于等于第一部分
}
//如果有剩下的
if(s)for(int j=v;j>=s*vi;--j)f[j]=max(f[j-s*vi]+s*wi,f[j]);
}
cout<<f[v];
return 0;
}
总结一下,二进制优化的东东一般都是将十进制数拆成2的整数幂
法三 单调队列优化\(O(NM)\)
大家可以去看下大佬的题解,写的很详细。
假设背包的体积为20,现在遍历到的物品体积为3,一共有4个,价值为w(因为无关紧要),我们来看下每个状态的求解是否有一定的规律。
f[i][0]=f[i-1][0]
f[i][1]=f[i-1][1]
f[i][2]=f[i-1][2]
以上状态都装不下体积为3的物品
f[i][3]=max(f[i-1][3],f[i-1][0]+w)
f[i][4]=max(f[i-1][4],f[i-1][1]+w)
f[i][5]=max(f[i-1][5],f[i-1][2]+w)
f[i][6]=max(f[i-1][6],f[i-1][3]+w,f[i-1][0]+2w)
f[i][7]=max(f[i-1][7],f[i-1][4]+w,f[i-1][1]+2w)
f[i][8]=max(f[i-1][8],f[i-1][5]+w,f[i-1][2]+2w)
f[i][9]=max(f[i-1][9],f[i-1][6]+w,f[i-1][3]+2w,f[i-1][0]+3w)
...
观察红色的状态,是不是发现了什么