背包问题是动态规划中常见且经典的问题。它有多种变种形式,接下来本人将对DP背包问题进行详细解说。
1.01背包
题目:有一个背包的容积为V,有N个物品,每个物品的体积为w[i],权重为v[i],每个物品只能取1次放入背包中,背包所有物品权重和最大是多少?
分析
使用动态规划来解决0-1背包问题时,可以按照以下步骤进行:
步骤1:定义状态
定义状态dp[i][j]
表示前i个物品放入容量为j的背包中所能获得的最大价值。
步骤2:确定状态转移方程
根据问题的最优子结构性质,可以使用以下状态转移方程:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
其中,w[i]
表示第i个物品的重量,v[i]
表示第i个物品的价值。状态转移方程表示在考虑第i个物品时,可以选择将其放入背包(此时总重量为j-w[i]
)或不放入背包,取两者中较大的价值。
步骤3:初始化边界条件
对于状态dp[0][j]
和dp[i][0]
,可以将其初始值设为0,表示没有物品或背包容量为0时的最大价值为0。
步骤4:进行状态转移
使用循环遍历所有可能的状态,从边界向目标状态逐步计算出问题的最优解。外层循环遍历物品,内层循环遍历背包容量。
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= C; j++) {
if (j >= w[i]) {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
步骤5:返回结果
根据问题的要求,返回最终的问题解dp[N][C]
,其中N
表示物品的数量,C
表示背包的容量。
so,代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1010;
int v[N],w[N];//分别表示第i物品的体积和价值
int f[N][N];//表示前i个物品,在体积不超过j的情况下的最大值
int main()
{
int n,m;//输入n个物品 总体积为m
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d%d",&v[i],&w[i]);
for(int i=1;i<=n;i++)//对n个物品从1到n一次枚举
for(int j=0;j<=m;j++)//体积在每个j下 能达到价值的最大值
{
if(j<v[i]) f[i][j]=f[i-1][j];
else f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
}
printf("%d",f[n][m]);
return 0;
}
但是我们会发现当前的代码使用了二维数组,导致空间复杂度特别高,那有没有办法减轻空间复杂度呢?当然有,我们可以发现f数组每次运算只会用到上一次循环中的数据,所以我们可以进行如下优化(将f数组优化到一维):
首先我们把刚刚的代码有关i的去掉
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
{
if(j<v[i]) f[j]=f[j];
else f[j]=max(f[j],f[j-v[i]]+w[i]);
}
然后我们能发现f[j]=f[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]);
}
但是现在循环又出现了一个问题,因为j-v[i]小于j[i],所以j-v[i]会先于j[i]更改,那要怎样避免这个问题呢?我们发现只要让j反过来遍历就可以使s[j]在j-v[i]前更新了。所以最终代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1010;
int v[N],w[N];
int f[N];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d%d",&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]);
}
printf("%d",f[m]);
return 0;
}
2.完全背包
题目:有一个背包的容积为V,有N个物品,每个物品的体积为v[i],权重为w[i],每个物品可以取无限次放入背包中,背包所有物品权重和最大是多少?
01背包问题和完全背包问题的区别就在于,每个物品取的最大次数是1次还是无限次。
根据01背包我们可以直接推导完全背包的状态和转移方程:
状态: f[i][j] 选择前i个物品,体积为j时的最优方案,即所选物品的最大权重和。
状态转移:f[i][j] = max(f[i-1][j- k * v[i]]+ k * w[i] (k= 0, 1, 2, 3, 4,...))
好了,恭喜你得到了初级代码:
#include<bits/stdc++.h>
int f[1050][1050];
int v[1050];
int w[1050];
int main()
{
int n,m;
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d %d", &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 (v[i]>j) continue;
for (int k = 0; k * v[i] <= j; k++) {
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
printf("%d\n", f[n][m]);
}
当然,和01背包一样,完全背包也可以进行空间优化,还是像01背包一样,第i轮的运算只需要用到i-1轮的数据,其次,我们还可以注意到v[i]>j这部分可以直接加入第二重循环,把从1到m的遍历改成从v[i]到m,为什么是从v[i]到m而不是从m到v[i]呢?因为在这道题中我们可以使用无数次物品,所以可以让当前元素取之前元素的最优解,不用考虑物品使用几次
然后我们就得到了最终代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,v[1001],w[1001],f[1001];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d%d",&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]);
}
}
printf("%d\n",f[m]);
}
3.多重背包
题目:有一个背包的容积为V,有N个物品,每个物品的体积为v[i],权重为w[i],每个物品可以取s[i]次放入背包中,背包所有物品权重和最大是多少?
这道题在01背包的基础上就特别好做了,如果我们每一个物品都从0遍历到s[i]判断使用次数就会超时,所以我们可以转成二进制的形式,说人话就是把每一个s[i]转成一堆不同的2的幂相加,然后用每一个幂乘上价值和重量,然后用01背包的模板求解,话不多说直接上代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,v[2001],w[2001],l[2001],f[2001],c[2001];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d%d%d",&v[i],&w[i],&l[i]);
}
for(int i=1;i<=n;i++){
int tot=0,x=1;
while(l[i]){
if(x<=l[i]) c[++tot]=x,l[i]-=x;
else c[++tot]=l[i],l[i]=0;
}
/*
for(;x<=l[i];x*=2)
c[++tot]=x,l[i]-=x;
c[++tot]=l[i];
*/
for(int k=1;k<=tot;k++){
for(int j=m;j>=v[i]*c[k];--j){
f[j]=max(f[j],f[j-v[i]*c[k]]+w[i]*c[k]);
}
}
}
printf("%d\n",f[m]);
}