传说中的背包有9讲,这里我们就介绍一下最基础的3讲
这三种背包是最经典的,也是我们经常碰到的,也是初学dp经常容易蒙圈的题目类型。
01背包
最经典的题目就是: 有n件物品,每件物品的重量为w[i],价值为v[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。(其中每种物品都只有一件)
我们设一个二维数组dp[i][j]来记录放入i件物品背包,放入重量为j的物品。
假设现在的背包的容量是 V=10;
物品编号:1 2 3
物品重量:5 6 4
物品价值:4 5 3
我们先考虑dp[0][j]表示一件物品不放的情况,那肯定就都为0
dp[i][0]表示放入i件重量为0的物品,dp[i][0]也就肯定都为0
然后我们就先考虑放一件物品的情况
然后再考虑放入两件的情况
最后再考虑放入三件的情况
我们先模拟一下过程,表格中的值为最大价值即dp[i][j]
物品数量/背包容量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 3 | 4 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 |
2 | 0 | 0 | 0 | 0 | 3 | 4 | 5 | 5 | 5 | 7 | 8 | 9 | 9 | 9 | 9 | 9 |
3 | 0 | 0 | 0 | 0 | 3 | 4 | 5 | 5 | 5 | 7 | 8 | 9 | 9 | 9 | 9 | 12 |
这里的话我们就模拟了取值的整个过程,可以从表中看出,我们假定取k个物品,背包的容量为V,按物品的重量(重量相同价值大的放入)依次放入,
我们先确定取k个物品,放入时先判断背包的容量是否满足能够放入物品,如果容量够,则放入背包,不够的话则增加背包的容量,这里的话就得注意一下了,如上表放入数量为2,背包重量为10的时候,我们这里就出现了一个问题,编号2、3的价值比1、3的更高,这个时候我们就得进行替换。
那么问题就来了,怎么判断是否进行替换呢,这就是01背包的精髓了
我们观察上表,就可以发现
dp[1][4] = dp[0][4]+v[3]
dp[2][10] = dp[1][10]+v[3]
dp[2][11] = dp[1][11]+v[1]
dp[2][4] = dp[1][4]
dp[3][14] = dp[2][14]
很明显我们就得到递推公式
背包容量还有时:dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i])
背包容量不足时:dp[i][j]=dp[i-1][j]
通俗的来说,就是通过得到的递推公式一层一层的更新不同物品数量下不同背包容量下的最大价值。
到这里,我们也就很容易用代码实现了
#include<bits/stdc++.h>
using namespace std;
int w[105],v[105];
int dp[105][1005];
int main()
{
int V,n,res=-1;
scanf("%d%d",&V,&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&w[i],&v[i]);
}
for(int i=1;i<=n;i++)
for(int j=V;j>=0;j--)
{
if(j>=w[i])//判断是否能将重量为w[i]的物品放到背包里
{
dp[i][j]=max(dp[i-1][j-w[i]]+v[i],dp[i-1][j]);
}
else
{
dp[i][j]=dp[i-1][j];
}
}
printf("%d",dp[n][V]);
return 0;
}
看完代码是不是发现了一个问题呀,就是其实没必要开二维数组,因为我们是一层一层更新(观察代码可以发现其实与i是没有关系的),我们可以就使用一维数组,在此基础上进行更新即可。
那是不是直接把二维去掉一维的就行呢,你品,你细品
假设现在的背包的容量是 C=10;
物品编号:1 2 3
物品重量:5 6 4
物品价值:20 10 12
我们先分析一下dp数组(从后到前更新)
dp:0 0 0 0 0 0 0 0 0 0
i=1:
dp[10] = max(dp[5]+20, dp[10]);
dp[9] = max(dp[4]+20, dp[9]);
dp[8] = max(dp[3]+20, dp[8]);
dp[7] = max(dp[2]+20, dp[7]);
dp[6] = max(dp[1]+20, dp[6]);
dp[5] = max(dp[0]+20, dp[5]);
dp: 0 0 0 0 20 20 20 20 20 20
i=2:
dp[10] = max(dp[6]+4, dp[10]);
dp[9] = max(dp[3]+10, dp[9]);
dp[8] = max(dp[2]+10, dp[8]);
dp[7] = max(dp[1]+10, dp[7]);
dp[6] = max(dp[0]+10, dp[6]);
dp: 0 0 0 0 20 20 20 20 20 20 //这里的话,选10的都被之前的20压下去了
i=3:
dp[10] = max(dp[6]+12, dp[10]);
dp[9] = max(dp[5]+12, dp[9]);
dp[8] = max(dp[4]+12, dp[8]);
dp[7] = max(dp[3]+12, dp[7]);
dp[6] = max(dp[2]+12, dp[6]);
dp[5] = max(dp[1]+12, dp[5]);
dp[4] = max(dp[0]+12, dp[4]);
dp: 0 0 0 12 20 20 20 20 32 32
再分析一下直接去掉一维的话(从前到后更新)
dp:0 0 0 0 0 0 0 0 0 0
i=1:
dp[5] = max(dp[0]+20, dp[5]);
dp[6] = max(dp[1]+20, dp[6]);
dp[7] = max(dp[2]+20, dp[7]);
dp[8] = max(dp[3]+20, dp[8]);
dp[9] = max(dp[4]+20, dp[9]);
dp[10] = max(dp[5]+20, dp[10]);
==dp: 0 0 0 0 20 20 20 20 20 40 ==
看到问题了吗!dp[10]不仅仅是由dp[5]决定了,因为dp[5]还被dp[0]更新了一次,相当于,i=1时,即只有一个物品时,这个物品拿了两次,完全不符合01背包了,但是,这个却是我们后面要提到的完全背包!接着看:
i=2:
dp[6] = max(dp[0]+10, dp[6]);
dp[7] = max(dp[1]+10, dp[7]);
dp[8] = max(dp[2]+10, dp[8]);
dp[9] = max(dp[3]+10, dp[9]);
dp[10] = max(dp[4]+10, dp[10]);
dp: 0 0 0 0 20 20 20 20 20 40
i=3:
dp[4] = max(dp[0]+12, dp[4]);
dp[5] = max(dp[1]+12, dp[5]);
dp[6] = max(dp[2]+12, dp[6]);
dp[7] = max(dp[3]+12, dp[7]);
dp[8] = max(dp[4]+12, dp[8]);
dp[9] = max(dp[5]+12, dp[9]);
dp[10] = max(dp[6]+12, dp[10]);
dp: 0 0 0 12 20 20 20 24 32 40
分析到这里应该就很明显了吧,dp[10] 就是背包容量为 10 的时候的最大价值,就是要求的值了,可以看到,容量大的时候的值取j决于容量小的时候的值,从而不断被正确更新,所以用一维 dp 的时候,j的循环必须是从大到小逆序开始的,就__防止了一个物品放入多次__!!!
(实在不理解的话可以输出每轮的结果看一下)
#include<bits/stdc++.h>
using namespace std;
int dp[1005],res=-1;
int w[1005],v[1005];
int main() {
int V,n;
scanf("%d%d",&V,&n);
for(int i=1; i<=n; i++) {
scanf("%d%d",&w[i],&v[i]);
}
for(int i=1; i<=n; i++) {
for(int j=V; j>=0; j--) {
if(j>=w[i])
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
printf("%d",dp[V]);
return 0;
}
下面是一些01背包的入门题目(按易→难)
裸背包问题
https://www.luogu.com.cn/problem/P1048
https://www.luogu.com.cn/problem/P1060
https://www.luogu.com.cn/problem/P1049
隐藏的01背包
https://www.luogu.com.cn/problem/P1734
求方案数
https://www.luogu.com.cn/problem/P1164
https://www.luogu.com.cn/problem/P1466
完全背包
完全背包的题面与01背包几乎完全一样,完全背包与01背包的差别就在于,完全背包中每种物品都可以无限次的拿取,01背包的话每种物品只能拿一次。 01背包中我们也分析过j的那层循环为了避免重复取同一个物品就得从后往前更新,但是完全背包可以重复取,哦哦哦!!! 是不是发现什么了呀!!!__每层我们只需要从头开始更新即可__#include<bits/stdc++.h>
using namespace std;
int n,m;
int c[100005],v[100005],dp[100005];//物品重量,价值
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>c[i]>>v[i];
}
for(int i=1;i<=m;i++){
for(int j=c[i];j<=n;j++){ //只有这里一个地方与01背包不同
dp[j]=max(dp[j],dp[j-c[i]]+v[i]);
}
}
printf("%d",dp[n]);
return 0;
}
01背包懂了的话完全背包就好理解了(您要是还不理解的话,蒻蒟好像也没有好的办法了V-V)
下面是蒟蒻推荐的裸完全背包题目
https://www.luogu.com.cn/problem/P1616
分组背包
分组背包顾名思义,就是把要装入背包的物品分成不同的组,每组中的物品互相冲突,最多选一件,然后放入背包中,然后求出最大价值。
这个很明显就是01背包的变形,可以理解为01背包是分组背包的一个特例,每个组里都只有一种选择。
首先判断一个分组当中的一件物品,同01背包一样,此物品存在两种状态,取与不取,若取此物品,则继续判断下一组的第一件物品,若不取此物品,则继续判断本组下一件物品,若该物品为本组最后一件物品,则判断下一组。也就是说设dp[i][j]表示前i组物品花费费用j能取得的最大权值,则有:dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[k]]+v[k]) (k属于第i组)
方程的意义是选择了前i组,用了容积为j的空间所能获取的最大价值把它转化为一维的便可以得到:dp[j]=max(dp[j],dp[j-w[k]]+v[k]) (k属于第i组)
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
for(int k=1;k<=a[i];k++)//第i组对应的k
dp[j]=max(dp[j],dp[j-w[k]]+v[k])
}
}
蒻鸡推荐的题目:
裸分组背包
https://www.luogu.com.cn/problem/P1757
依赖性背包
https://www.luogu.com.cn/problem/P1064
总结:背包的性质都是类似的,其他的背包都是以01背包作为基进行拓展的,因此一定要好好理解01背包并熟练运用,有问题的童鞋欢迎交流~