背包九讲——完全背包问题
完全背包问题呢,见名知意,就是所谓的物品无限多,选也选不完的那种,是多重背包的promax版本。完全背包问题是背包问题的一种变体,与0/1背包问题有所不同。在完全背包问题中,每种物品的数量是无限的,可以选择任意数量的某一种物品放入背包中。
目录
背包问题第三讲——完全背包问题
背包问题是一类经典的组合优化问题,通常涉及在限定容量的背包中选择物品,以最大化某种价值或利益。问题的一般描述是:有一个背包,其容量为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
,我们有两种选择:
- 不选择第
i
个物品。 - 选择第
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博客
背包问题是经典之经典,每一位算法入门学者必学的内容,里面的优化涉及到的也非常具有思维性,值得大家好好学习。由于笔者水平有限,一些方面可能也存在着问题,望大家理解支持,有错误就指出改正,大家一起进步,执笔至此,感触彼多,全文将至,落笔为终,感谢各位的支持,下篇更新混合背包问题。
更多推荐
所有评论(0)