在之前回顾动态规划算法时呢,涉及到了背包问题,背包问题作为动态规划中最经典的问题,有助于深入理解动态规划。本文基于背包九讲和作者平时的经验进行了一定的背包问题总结,总结不够到位的地方,还请大家指出探讨。
作为很多问题的母体,背包问题具有的共性为:
一共有N个物品,有一个容量为M的背包,这N个物品,分别占用Vi的容量和具有Pi的价值,要求组合出不超过背包容量的物品,使其价值最大。
01背包问题:
每一件物品最多选取一次。
对于这一类问题呢,我们要枚举出所有的情况,不能选择的物品为0,能够选择的物品为1。
基本思路:
①首先将原问题拆解为子问题
原问题为:N个物品中选,放在体积为V的背包中,总价值最大
子问题:从前i个物品中选,放在体积为j的背包中,总价值最大
②定义状态及初始状态:使用a[i][j]表示前i个物品,背包容量为j时,最大的价值; 初值全为0
③确定转移方程:由于a[i][j]表示前i个物品中,容量为j时能够放下的最大价值,所以,如果不能够放入第i个物品,那么直接f[i,j]=f[i-1,j]即可,如果能够放下第i个物品(即j向右扫),我们只需要选择不放入第i个物品和放入第i个物品(值为恰好去掉第i个物品时的最大值+第i个物品的值)的最大值即可,即
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i])
题目来源:AcWing 01背包问题
基本算法实现:dp+二维数组
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int w[1010]; // 体积
int v[1010]; // 价值
int f[1010][1010]; // f[i][j], j体积下前i个物品的最大价值
int main()
{
int n, m;
// cin >> n >> m;
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i++) {
// cin >> w[i] >> v[i];
scanf("%d%d",&w[i],&v[i]);
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++){
// 当前体积装不进,价值等于前i-1个物品
if(j < w[i])
f[i][j] = f[i-1][j];
// 可以装下,选择最大值
else
f[i][j] = max(f[i-1][j], f[i-1][j-w[i]] + v[i]);
}
}
// cout << f[n][m];
printf("%d",f[n][m]);
return 0;
}
时间复杂度O(mn)
模板优化
从基本算法中不难发现,数组f第i行的元素全是由第i-1行变化的来的,所以在基本算法实现中将新的值放入到新的一行中就显得不是很有必要了,因而我们可以采用一维滚动算法来实现优化。故状态转移方程可以修改为
但是需要注意的是,我们在采用这种方法进行更新后,如果体积大小依然是采用正序变化的话,由于j从小向大进行演变,所以会不断更新f[j]的值,而这个时候当遇到了可以放下的体积,即j-w[i]的时候,再次使用的f[j]则是使用的第i行的情况了,则不符合基本算法中使用的第i-1行值了,因而我们在这里使用逆序的方法来避免这个问题
故优化后的代码模板为
for(int i=1;i<=n;i++)
{
for(int c=w[i];c>=0;c--)
{
f[c]=max(f[c],f[c-w[i]]+v[i]);
}
}
完全背包问题:
完全背包问题呢,则是所有的物品都可以不限制次数的放入到背包中
所以在能否装入的判断上,我们需要与k*w[i]进行比较,此外与01背包问题不同的是,我们在做判断最大值的时候,则不是与前i个物品的最大值进行比较,因为当前物品放入不同个数时最大值也不同,故而转移方程为
f[i][j]=max(f[i][j],f[i][j-k*w[i]]+k*v[i])
暴力算法实现
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int w[1010]; // 体积
int v[1010]; // 价值
int f[1010][1010]; // f[i][j], j体积下前i个物品的最大价值
int main()
{
int n, m;
// cin >> n >> m;
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i++) {
// cin >> w[i] >> v[i];
scanf("%d%d",&w[i],&v[i]);
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++){
//能够装进的时候,比较之前的最大值即可
for(int k=0;k*w[i]<=j;k++){
f[i][j]=max(f[i][j],f[i-1][j-k*w[i]]+k*v[i]);
}
}
}
// cout << f[m][n];
printf("%d",f[n][m]);
return 0;
}
时间复杂度O(nm^2)
模板优化:
可以注意到,在暴力算法实现中呢,对于第i个物品放入n个时,价值变化是由第n-1个变化的来的,所以其实在最深层嵌套的部分中的k就不是很有必要存在了。故可以将k去除掉
因而可以考虑使用滚动数组进行优化
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++){
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);
}
}
时间复杂度为O(mn)
通过之前01背包问题的优化思路呢,我们可以发现01的正序遍历正好是符合完全背包问题的优化的,故而优化后的代码为
for(int i = 1 ; i<=n ;i++){
for(int j = v[i] ; j<=m ;j++){//注意了,这里的j是从小到大枚举,和01背包不一样
f[j] = max(f[j],f[j-v[i]]+w[i]);
}
}