用了两天的时间把讲解背包问题的视频看了一遍,加深了自己对dp的理解和认识。dp问题可以从两个方面入手,状态表示 状态计算 而背包问题的状态表示一般都是两维 dp[i][j]表示从前i个物品中选且总体积不超过j的最大值,而背包问题的状态计算也是大同小异。
01背包
01背包是最简单的背包问题,就是每件物品最多只能取一次,这样就可以得出01背包的状态转移方程 dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]),这便是01背包最基础的解法
int slove1(){ //朴素做法 时间复杂度o(n)^2;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
dp[i][j]=dp[i-1][j];
if(j>=v[i])
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
}
}
return dp[n][m];
}
但是又可以看出,dp数组的第一维永远都是i-1,所以我们可以把第一维度去掉然后就可以得出一维的01背包解法
int slove2(){//优化一维做法 时间复杂度o(n)2;
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]);
}
}
return f[m];
}
在写一维的01背包解法的时候要注意,原转移方程中的dp[i][j]是与第i-1层进行比较的,所以我们应该从大到小枚举j,确保当前的j是没有被更新过的j;
完全背包
完全背包问题是指所有物品的数量是无限的,容易得出完全背包的状态计算为dp[i][j]=max(dp[i-1][j-kv[i]]+kw[i]) k=(0,1,2,3…),不难写出完全背包的朴素写法
int slove1(){ //朴素做法 o(n)^3
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
for(int x=0;x*v[i]<=j;x++){
if(x*v[i]<=j)
dp[i][j]=max(dp[i][j],dp[i-1][j-x*v[i]]+x*w[i]);
}
}
}
return dp[n][m];
}
dp[i][j]=max(dp[i-1][j],dp[i-1][k-v[i]]+w[i],dp[i-1][k-2v[i]]+2w[i]…)
dp[i][j-v[i]]=max( dp[i-1][k-v[i]], dp[i-1][k-2v[i]]+w[i])
通过上面两个式子可以发现dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i])
从而得到优化后的完全背包
int slove2(){ //优化做法 o(n)^2;
for(int i=1;i<=n;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][j-v[i]]+w[i]);
}
}
return dp[n][m];
}
根据将01背包聪二维优化为一维的经验,我们可以发现完全背包也可以将二维优化为一维,只不过完全背包的第一维为第i层,所以要从小到大枚举j
int slove3(){//将二位优化为一维数组,因为f[j-v[i]]是第i层的,所以要从小到大枚举j
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]);
}
}
return f[m];
}
多重背包
多重背包即每个物品的数量是有限的,第i个物品的数量是si,其实多重背包的转移方程跟完全背包的基本一致,就是多个判断数量的条件
dp[i][j]=max(dp[i-1][j-kv[i]]) k=(0,1,2,3…)
#include<iostream>
#include<string.h>
#include<algorithm>
#include<stdio.h>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
#pragma comment(linker, "/STACK:102400000,102400000")
typedef long long ll;
using namespace std;
const int N=1e2+10;
int v[N],w[N],s[N],dp[N][N],f[N];
int n,m;
int slove(){//朴素做法 时间复杂度o(n)^3;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
dp[i][j]=dp[i-1][j];
for(int x=0;x*v[i]<=j&&x<=s[i];x++){
dp[i][j]=max(dp[i][j],dp[i-1][j-x*v[i]]+x*w[i]);
}
}
}
return dp[n][m];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i]>>s[i];
}
int ans=slove1();
cout<<ans;
}
有了二维转一维的经验,我们不难发现多重背包转一维和完全背包转一维基本类似,但是不要照搬,因为我们还需要考虑的一个条件就是第i件物品的个数为si个。
假设第1件物品有1023个,那么我们可以分组
1 2 4 8 16 32 64 128 256 512
我们可以发现第一组和第二组按照需要的方式结合可以得到1——3中的任意一个数,之后在于第三组结合能得到1——7中的任意一个数,后面的以此类推,可以发现这10组的自由结合可以得到1——1023中的任意一个数,所以我们可以把这些组都存起来,v[1]=1v[i],v[2]=2v[i]…;将这些数都存起来,我们就得到了一个类似01背包的问题,那么问题就迎刃而解了
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 12000;
int v[N],w[N],f[N],s[N];
int n,m;
// 多重背包一维做法
int main(){
cin>>n>>m;
int cnt=1;
int q,y,s;
for(int i=1;i<=n;i++){
cin>>q>>y>>s;
int x=1;
while(x<s){
v[cnt]=x*q;
w[cnt]=x*y;
s-=x;
x=x*2;
cnt++;
}
if(s!=0){
int res=s;
v[cnt]=s*q;
w[cnt]=s*y;
cnt++;
}
}
for(int i=1;i<=cnt;i++){
for(int j=m;j>=v[i];j--){
f[j]=max(f[j],f[j-v[i]]+f[i]);
}
}
cout<<f[m];
}
由于我们要将所有组都存入 v和w数组,所以我们要将这个数组开大一点。
分组背包
相信能看到这里,一定对背包有了较深入的认识,所以下面我就不会再那么细致的讲解了
分组背包:每一组有若干个物品,但每组最多只能取一个。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1e2+10;
int s[N],v[N][N],w[N][N],dp[N][N];
int f[N];
int n,m;
//分组背包,每一组只能取一个
int slove1(){//朴素写法 时间复杂度o(n)^3;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
dp[i][j]=dp[i-1][j];
for(int x=1;x<=s[i];x++){
if(j>=v[i][x])
dp[i][j]=max(dp[i][j],dp[i-1][j-v[i][x]]+w[i][x]);
}
}
}
return dp[n][m];
}
int slove2(){//优化为一维写法
for(int i=1;i<=n;i++){
for(int j=m;j>=1;j--){
for(int x=1;x<=s[i];x++){
if(j>=v[i][x])
f[j]=max(f[j],f[j-v[i][x]]+w[i][x]);
}
}
}
return f[m];
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s[i];
for(int j=1;j<=s[i];j++){
cin>>v[i][j]>>w[i][j];
}
}
int ans=slove2();
cout<<ans;
}