动态规划的思想是非常容易理解的,但是对于某个问题要想到对应的递推式往往不那么容易。一看就会,一做就懵。学习过01背包、完全背包,可是在我脑中的印象似乎并不那么深刻,为了巩固与提升,看了B站某大佬讲解,讲得细致、透彻,收获颇丰,原来背包问题也没有那么的困难。为了巩固与提升,写一篇博客记录一下。
以下题目链接ACwing
设dp[i][j]为前i件物品共选中体积为j物品的最大价值。
对于每一个物品都有选和不选两种情况,那么就有递推式dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]]+w[i])
#include
#include
#define MAX 1010
using namespace std;
int N,M;
int dp[MAX][MAX];
int main()
{
scanf("%d%d",&N,&M);
for(int i=1;i<=N;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=0;j<=M;j++)
{
dp[i][j] = dp[i-1][j];
if(j-v>=0)
dp[i][j] = max(dp[i][j],dp[i-1][j-v]+w);
}
}
printf("%d\n",dp[N][M]);
}
上述算法的时间复杂度为O(NM),已经不可以再优化了。但是空间复杂度还可优化。
dp[i][j]只与dp[i-1][j]与dp[i][j-v[i]]+w[i]有关。而当前dp[i][j]就是之后的dp[i-1][j]了,那么我们就可以把数组弄成一维。即dp[j]表示选中体积j的物品的最大价值。此外我们还需要将j变为由大到小遍历。因为如果j由小到大遍历,那么遍历时内层循环中的dp[j-v[i]]实际上等同于上面的的dp[i][j-v[i]],而不是dp[i-1][j-v[i]],而由大到小的遍历保证了在当前的dp[j-v[i]]是前一个外层循环的值。
于是:
#include
#include
#define MAX 1010
using namespace std;
int N,M;
int dp[MAX];
int main()
{
scanf("%d%d",&N,&M);
for(int i=1;i<=N;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=M;j>=v;j--)
{
dp[j] = max(dp[j],dp[j-v]+w);
}
}
printf("%d\n",dp[M]);
}
将01背包的代码的j改为由小到大遍历即可。
#include
#include
#define MAX 1010
using namespace std;
int N,M;
int dp[MAX];
int main()
{
scanf("%d%d",&N,&M);
for(int i=1;i<=N;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=v;j<=M;j++)
{
dp[j] = max(dp[j],dp[j-v]+w);
}
}
printf("%d\n",dp[M]);
}
dp[j] = max{dp[j-k*v[i]]+w[i]}, 0<=k<=s[i]
于是有以下代码
#include
#include
#define MAX 110
using namespace std;
int N,M;
int dp[MAX];
int main()
{
scanf("%d%d",&N,&M);
for(int i=1;i<=N;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
for(int j=M;j>=v;j--)
{
for(int k=0;k<=s&&k*v<=j;k++)
{
dp[j] = max(dp[j],dp[j-k*v]+k*w);
}
}
}
printf("%d\n",dp[M]);
}
代码1的复杂度为O(NMS),实际上我们可以把它优化为O(NMlog(S))。对于每一件物品,最多可能取s件,而0~s可以由log(s+1)个不同的数字表示。 例如0-7可以由1,2,4三个数表示。而0-10可以由1,2,4,10-1-2-4=3完全表示。然后就可以转换成01背包问题。
于是有:
#include
#include
#include
#define MAX 2010
using namespace std;
int N,M;
int dp[MAX];
struct Good
{
int v;
int w;
};
int main()
{
scanf("%d%d",&N,&M);
vector<Good> goods;
for(int i=1;i<=N;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
for(int k=1;k<=s;k*=2)
{
s-=k;
goods.push_back(Good{k*v,k*w});
}
if(s>0)
goods.push_back(Good{s*v,s*w});
}
for(auto good : goods)
{
for(int j=M;j>=good.v;j--)
{
dp[j] = max(dp[j],dp[j-good.v]+good.w);
}
}
printf("%d\n",dp[M]);
return 0;
}
有点复杂,之后再更
显然,该问题可以转换为01背包和完全背包,代码如下:
#include
#include
#include
#define MAX 1010
using namespace std;
struct Good
{
int v;
int w;
int s;
};
int N,M;
int dp[MAX];
int main()
{
scanf("%d%d",&N,&M);
vector<Good> goods;
for(int i=1;i<=N;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
if(s<=0)
goods.push_back(Good{v,w,s});
else{
for(int k=1;k<=s;k*=2)
{
s-=k;
goods.push_back(Good{k*v,k*w,-1});
}
if(s>0)
goods.push_back(Good{s*v,s*w,-1});
}
}
for(auto good : goods)
{
if(good.s<0)
{
for(int j=M;j>=good.v;j--)
dp[j] = max(dp[j],dp[j-good.v]+good.w);
}
else{
for(int j=good.v;j<=M;j++)
dp[j] = max(dp[j],dp[j-good.v]+good.w);
}
}
printf("%d\n",dp[M]);
}
没什么好讲的,实际上还是一个01背包。
#include
#include
#define MAX 1010
using namespace std;
int N,V,M;
int dp[MAX][MAX];
int main()
{
scanf("%d%d%d",&N,&V,&M);
for(int i=1;i<=N;i++)
{
int v,m,w;
scanf("%d%d%d",&v,&m,&w);
for(int j=V;j>=v;j--)
{
for(int k=M;k>=m;k--)
{
dp[j][k] = max(dp[j][k],dp[j-v][k-m]+w);
}
}
}
printf("%d\n",dp[V][M]);
return 0;
}
dp[j]=max{dp[j],dp[j-v[i]]+w[i]|物品i属于第k组}
#include
#include
#include
#define MAX 1010
using namespace std;
int N,M;
int dp[MAX];
int main()
{
scanf("%d%d",&N,&M);
for(int i=1;i<=N;i++)
{
int S;
scanf("%d",&S);
vector<int> v,w;
v.resize(S);
w.resize(S);
for(int k=0;k<S;k++)
scanf("%d%d",&v[k],&w[k]);
for(int j=M;j>=0;j--)
for(int k=0;k<S;k++)
if(j-v[k]>=0)
dp[j] = max(dp[j],dp[j-v[k]]+w[k]);
}
printf("%d\n",dp[M]);
}
建一个数组g,g[i]表示体积恰为i的方案数
#include
#include
#define MAX 1010
using namespace std;
const int MOD = 1e9+7;
int dp[MAX], g[MAX];
int N,M;
int main()
{
scanf("%d%d",&N,&M);
dp[0] = 0;
g[0] = 1;
for(int i=1;i<=M;i++)
dp[i] = -0x3f3f3f3f;
for(int i=0;i<N;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=M;j>=v;j--)
{
int maxi = max(dp[j],dp[j-v]+w);
int sum = 0;
if(maxi==dp[j])
sum+=g[j];
if(maxi==dp[j-v]+w)
sum+=g[j-v];
dp[j] = maxi;
sum%=MOD;
g[j] = sum;
}
}
int maxians = 0;
for(int i=0;i<=M;i++)
maxians = max(maxians,dp[i]);
int res = 0;
for(int i=0;i<=M;i++)
{
if(dp[i]==maxians)
{
res+=g[i];
res%=MOD;
}
}
printf("%d\n",res);
}
为了字典序最小,应该由序号高的向低的遍历
#include
#include
#define MAX 1010
using namespace std;
int N,M;
int dp[MAX][MAX];
int v[MAX],w[MAX];
int main()
{
scanf("%d%d",&N,&M);
for(int i=1;i<=N;i++)
scanf("%d%d",&v[i],&w[i]);
for(int i=N;i>=1;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][j],dp[i+1][j-v[i]]+w[i]);
}
}
int vol = M;
for(int i=1;i<=N;i++)
{
if(vol-v[i]>=0)
{
//此条件为true意味着选中了i
if(dp[i][vol] == dp[i+1][vol-v[i]]+w[i])
{
if(vol!=M)
printf(" ");
vol-=v[i];
printf("%d",i);
}
}
}
printf("\n");
}