HDU2602 Bone Collector背包问题入门:01背包
题目
一共有 N 件物品,第 i 件物品的体积是 v[i],价值是 c[i] 。
有一个容量为 M 的背包。
在每件物品只能拿一次的前提下,求背包能装入物品的总价值最大为多少(并不要求背包装满,只要价值最大就行)?
基本思路
- 01背包是动态规划类问题。一般需要将主问题分解为多个子问题,然后记录下不同子问题的解,最终才能推得最终问题的答案。
- 定义子问题:
前i件物品
恰放入一个容量为v的背包
可以获得的最大价值是多少? - 我们用 f[i][v] 表示 前i件物 恰放入一个 容量为v 的背包可以获得的 最大价值。
- 要求主问题f[i][v] 就得先知道子问题
F[i-1][v]
和f[i-1][ v-v[i] ]
是多少 。
然后通过状态转移方程:f[i][v]=max{ f[i-1][v] , f[i-1][ v-v[i] ]+c[i]] }
- 状态转移方程是整个背包问题的核心。解释:“前i个物品放入容量为v的背包中”可以转化为“前i-1个物品已经放入了容量为v的背包中(即为f[i-1][v])” 和 “第i个物品放还是不放入背包中?”
如果第i个物品不放入背包,那么f[i][v]=f[i-1][v]
如果第i个物品放入背包,那么f[i][v]=f[i-1][ v-v[i] ] + c[i],此时这个式子里面只有f[i-1][ v-v[i] ]未知,问题转化为“前i-1个物品放入了剩余容量为v-v[i]的背包中能获得最大价值是多少?”
二维数组表示代码
思路用二位数组储存子问题的答案,我们要求f[N][M]
,就得先求出所有的f[i][v]
.
这里的i
是由0到N,v
是由0到M。所以需要用双重for循环来求,i的变化为主循环,v的变化为次循环。这样就会一行一行地得到一个矩阵。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxN=1e3+10;
int f[maxN][maxN];///f[i][j]体积为j的背包在前i件物品中能装的最大价值
int v[maxN];///物品体积
int c[maxN];///物品价值
int main()
{
int t;
scanf("%d",&t);
while(t--){
int n,m;
memset(f,0,sizeof(f));
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&c[i]);///格物品价值
}
for(int i=1;i<=n;i++){
scanf("%d",&v[i]);///各物品体积
}
///计算所有子问题的答案。
for(int i = 1;i <= n; i++){
for(int j = 0;j <= m; j++){
f[i][j]=f[i-1][j];///体积为 i-1 可以拿f[i-1][j], 那体积比它大的 i 的至少也可以拿它那么多吧
if(j >= v[i])///防止f[j-v[i]]越界
f[i][j] = max(f[i-1][j] , f[i-1][j - v[i]] + c[i]);
}
}
printf("%d\n",f[n][m]);
}
return 0;
}
二维数组的缺点
时间复杂度已经不能优化了,为O(N*M)。但是二维数组的空间复杂度太高,我们可以将其改成 一维数组进行储存。
优化空间复杂度
- 二维数组方法中,由代码可知道,矩阵的每一行的其中每个元素f[i][j],都是由它的上一行的元素
f[i-1][j]
或者f[i][j-v[i]]
推得。我们用f[v]表示上面定义的f[i][v]
,相应的f[ v-v[i] ]表示上面提到的f[i][ v-v[i] ]
。 - 状态转移方程就变成了:
f[v]=max{ f[v] , f[ v-v[i] ] + c[i] }
特别注意: 等号后面的 f[v] 和前面那个f[v]
不一样。 后面的f[v]
是上一次循环得到的结果(相当于二维数组的前一行中的元素),而这一次循环所求的f[v]
需要由上一次循环得到的f[v]
推得(类比二维数组方法: 由上一行推出下一行 )。这里的f[ v-v[i] ]
也是上一次循环得到的结果(相当于二维数组方法里面的前一行中的元素)。
!!!!视频图片演示!!!!!!:
戳我看b站大佬演示视频
注意看,计算每一个新的子问题时,都用到了哪些位置上的已经推得的数据。
这样你就会明白:
为什么可以用一维数组替代二维数组保存?(因为每一行更新完,上一行的数据不再需要了,所以根本不需要将它一直储存起来。)
为什么一维数组的第二次循环到从大到小?!(接着往下看*_*)
一维数组表示代码
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxN=1e3+10;
int c[maxN],v[maxN];
int f[maxN];///前i个物品中体积为v所能获取的最大价值。
int main()
{
int t;
scanf("%d",&t);
while(t--){
int n,m;
scanf("%d%d",&n,&m);
///一定要初始化f数组,并且是全部初始化,不能因为想节约时间而只初始化一部分。
memset(f,0,sizeof(f));
///输入已知数据
for(int i=0;i<n;i++)
scanf("%d",&v[i]);///物品体积
for(int i=0;i<n;i++)
scanf("%d",&c[i]);///物品价值
///计算子问题答案。
for(int i=0;i<n;i++){///前i个物品
for(int j = m; j >= v[i]; j--){ ///体积从m->0(实际上要从m->V[i],防止f[ j-v[i] ]越界)
f[j]=max(f[j] , f[ j-v[i] ] + c[i] );
}
}
printf("%d\n",f[m]);///体积为m的背包能装下的最大价值
}
return 0;
}
代码中关键问题
上面代码的第二重循环条件为什么是从大的 M,到小的 v[i]? 为何不是由小到大呢?还是因为状态转移方程右边的f[ v-v[i] ]
表示 上一次主循环的结果,只有当它未被这一次主循环更新的时候,才能拿来用。如果你把逆序改成顺序的话,那么 f[ v-v[i] ]
这个表达式代表的上一次循环得到的值就 会被当前主循环所覆盖,变成了二维数组方法中的f[i][ v-v[i] ]
,而不是f[i-1][ v-v[i] ]
.
其他背包问题
其他背包问题就很简单啦,基本上都和01背包分不开,理解了01背包,学其他背包势如破竹。
上CSDN搜索"背包九讲完整版",下载个文档自己看看,哈哈哈。
没有积分下载的话找我,我发给你。菜鸟扣扣 : 2215782031