DP背包问题详解

背包问题是动态规划中常见且经典的问题。它有多种变种形式,接下来本人将对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]);
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值