目录

完全背包问题

问题定义

动态规划解法

状态转移方程

初始化

遍历顺序

三种解法:

朴素版——枚举k

进阶版——dp正推(一维滚动数组)


背包问题第三讲——完全背包问题

背包问题是一类经典的组合优化问题,通常涉及在限定容量的背包中选择物品,以最大化某种价值或利益。问题的一般描述是:有一个背包,其容量为C;有一组物品,每个物品有重量w和价值v。目标是选择一些物品放入背包,使得它们的总重量不超过背包容量,同时总价值最大。
完全背包问题则是每个物品都是无限个,而不是只有一个,永远取不完。

完全背包问题

完全背包问题呢,见名知意,就是所谓的物品无限多,选也选不完的那种,是多重背包的promax版本。完全背包问题是背包问题的一种变体,与0/1背包问题有所不同。在完全背包问题中,每种物品的数量是无限的,可以选择任意数量的某一种物品放入背包中。问题的描述如下:
给定一个背包容量为m,有n种物品,每种物品有重量v[i]和价值w[i],且数量无限。目标是选择物品放入背包,使得它们的总重量不超过背包容量,并且总价值最大。
与0/1背包问题相比,完全背包问题的状态转移方程有所不同,因为每种物品可以选择多次。
解决完全背包问题的方法与0/1背包问题类似,可以使用动态规划、贪心算法等。常见的动态规划方法包括自底向上的迭代和自顶向下的递归+记忆化搜索。

问题定义

给定:

  • 一组物品,每个物品有一个重量w[i]和价值v[i],其中i是物品的索引。
  • 一个背包的容量W

目标:

  • 选择一些物品放入背包,使得背包中物品的总价值最大,同时不超过背包的容量。

动态规划解法

动态规划数组dp[j]表示容量为j的背包所能容纳物品的最大价值。

状态转移方程

对于每个物品i,我们有两种选择:

  1. 不选择第i个物品。
  2. 选择第i个物品,由于物品可以无限取用,我们可以取用任意数量的第i个物品。

状态转移方程为:dp[j]=max(dp[j],dp[j−w[i]]+v[i]) 其中j是当前背包的容量,w[i]是第i个物品的重量,v[i]是第i个物品的价值。

初始化
  • dp[0] = 0,因为容量为0的背包没有价值。
遍历顺序
  • 遍历物品,对于每个物品,再遍历背包容量。

三种解法:

既然是promax版本,那还是离不开01背包啊,既然我可以无限选,那就可以选到知道背包装不下为止,就是m/v[i]。

例题就用acwing上的完全背包问题:3. 完全背包问题 - AcWing题库​​​​​​


朴素版——枚举k

最先想到的就是简单的枚举k了吧,把完全背包转换成多重背包,我们的物品是无限多个,但是我们的背包容量是有限的,背包容量有限的话,那么我所能装下的物品就是有限个,每一个物品都有一个限定的值,这个值含义是背包只装第i种物品所能装的最多的个数,我们把所有物品的最大数求出来,那么此题就变成了多重背包问题了,每个物品最多枚举到m/v[i],相当于每个物品的个数确定了,可以利用多重背包的二进制优化或者单调队列优化。

#include<iostream>
using namespace std;
int dp[1005],a[1005];
int n,m;
int v[1005],w[1005];
int main(){
	cin>>m>>n;
	for(int i=1;i<=n;i++){
		cin>>v[i]>>w[i];
	}
	for(int i=1;i<=n;i++){
		for(int j=m;j>=1;j--){
			for(int k=0;k<=j/v[i];k++){
				if(j>=k*v[i]){
					dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);
				}
			}
		}
	}
	cout<<dp[m]<<endl;
	return 0;
}

如果是这样枚举k的朴素版本,那么肯定过不了,时间复杂度太大,考虑优化。


进阶版——dp正推(一维滚动数组)

用一个一维滚动数组,第一个for循环枚举物品个数,第二个for循环去枚举背包容量,枚举的边界为v[i]到m,因为当背包容量>=v[i]的时候,我才能选择第i个物品,后面就是随着第i个物品的个数不断增加,每当一种新的物品加入进来,就意味着数组要滚动一次,在上一个的状态(前i-1种物品)的基础上去更新加入第i个物品的情况,求最优解。

#include<iostream>
using namespace std;
int dp[1005];//dp[i]表示背包容量为i是最大价值
int n,m;//n个物品m背包容量
int v[1005],w[1005];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>v[i]>>w[i];
	}
	for(int i=1;i<=n;i++){
		for(int j=v[i];j<=m;j++){//这里从v[i]到m,保证了能选1——m/v[i](最多)
					dp[j]=max(dp[j],dp[j-v[i]]+w[i]);//状态转移方程
		}
	}
	cout<<dp[m]<<endl;
	return 0;
}

下面解释一下为啥要正序,因为正序的话从小到大更新,在更新的时候状态可以从小的状态转移过来。也就是说在更新dp[i]的时候,i前面的状态(dp[0]---dp[i-1])都被求出来了,那么我们可以利用这一点,当前状态可以从前面已经求出来的状态进行状态转移。

 这样的话时间复杂度大大降低,优化掉了那层k循环,时间复杂度O(nm)

视频讲解这个B站有动画的笔者感觉挺好【信息学奥赛教程】完全背包问题_哔哩哔哩_bilibili

上一篇内容为多重背包问题: 背包九讲——多重背包问题-CSDN博客 


背包问题是经典之经典,每一位算法入门学者必学的内容,里面的优化涉及到的也非常具有思维性,值得大家好好学习。由于笔者水平有限,一些方面可能也存在着问题,望大家理解支持,有错误就指出改正,大家一起进步,执笔至此,感触彼多,全文将至,落笔为终,感谢各位的支持,下篇更新混合背包问题

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐