背包问题
weichenglu · · 算法·理论
背包问题
背包问题是一个经典的动态规划的问题,大致是给定一个背包的容量与一些物品的重量和价值。要求在物品总重量总和不超过背包容量的前提下,使所得物品总价值最大。
背包问题大致分为以下几种类型:01 背包、完全背包、多重背包、分组背包等。
01 背包
01 背包问题是背包问题中最基础,也是最简单的一类问题。它的主要特点为:对于每个物品,要么选,要么不选。因此被称为 01 背包。
经典问题如下:
有
方法 1——枚举(暴搜)
写一个深搜程序,对
int ans = 0;
void dfs(int i,int vsum,int wsum){
// i -> 搜索到第 i 个物品 vsum,wsum -> 分别为目前已选物品的体积、价值之和
if (i == n + 1){
if (vsum <= m) ans = max(ans,wsum);
return;
}
if (v[i] + vsum <= m) dfs(i+1,vsum+v[i],wsum+w[i]); //选
dfs(i+1,vsum,wsum); // 不选
}
方法 2——
当我们考虑了前
考虑前
-
不选第
i 个物品,问题就变成了考虑前i-1 个物品总体积为j 的情况; -
选第
i 个物品,问题就变成了考虑前i-1 个物品总体积为j-v_i 的情况。
综上所述:可得状态转移方程式:
写出代码:
int dp[N][M]; //其中N和M分别代表n和m的最大值
for (int i=1;i<=n;++i){
for (int j=0;j<=m;++j){
if (j < v[i]) dp[i][j] = dp[i-1][j];
else dp[i][j] = max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
}
}
但是,这样的代码过于复杂,我们可以用以下几个技巧优化,同时,这些技巧是动态规划的常用优化技巧,第一个特别常用!!!
技巧 1——滚动数组
注意到上面代码的空间复杂度为
再看到代码,发现考虑前
因此,我们可以使用滚动数组。对于本题,可以使用以下两种方法:
//方法一:
int f[M],g[M];
// f 数组代表前 i-1 个物品的状态
// g 数组代表前 i 个物品的状态
for (int i=1;i<=n;++i){
for (int j=0;j<=m;++j){
if (j < v[i]) g[j] = f[j]; //注意中括号内的变量!!!
else g[j] = max(f[j],f[j-v[i]]+w[i]);
}
memcpy(f,g,sizeof(g)); // 将 g 复制一份给 f
}
cout << f[m]; // 等价于:cout << g[m]
// 方法二:
int dp[2][M];
for (int i=1;i<=n;++i){
for (int j=0;j<=m;++j){
if (j < v[i]) dp[(i)&1][j] = dp[(i-1)&1][j];
else dp[i&1][j] = max(dp[(i-1)&1][j],dp[(i-1)&1][j-v[i]]+w[i]);
}
}
cout << dp[n&1][m];
一般来讲,第二种会更加常用,也更加好用。
技巧 2——逆序滚动数组
先看代码:
int dp[M];
for (int i=1;i<=n;++i){
for (int j=m;j>=a[i];--j){
dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
}
}
来解释一下原因:(为区分滚动数组优化和逆序滚动数组优化的代码区别,滚动数组优化中的
如图,红底和蓝底部分表示目前
先看
再看
综上所述:两段代码等价。(后面的代码基本会也只会使用这种写法)
这样,01背包的状态转移方程式就可以变为(一定要注意,这里是逆序!)
模板题
P1048 采药
需要注意的是:这道题目的
正确代码:
#include <bits/stdc++.h>
#define N 105
#define M 1005
using namespace std;
int n,m;
int w[105],v[105];
int [1005];
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> m >> n;
for (int i=1;i<=n;++i) cin >> w[i] >> v[i];
for (int i=1;i<=n;++i){
for (int j=m;j>=w[i];--j){
[j] = max([j],[j-w[i]]+v[i]);
}
}
cout << [m];
return 0;
}
完全背包
完全背包是背包问题的另一种问题,区别于01背包,完全背包的特点是:对于每种物品,可以取任意件。
经典问题如下:
有
参考01背包的做法,依旧考虑前
-
不选第
i 种物品,问题就变成了考虑前i-1 种物品总体积为j 的情况; -
选第
i 种物品,问题就变成了考虑前i 种物品总体积为j-v_i 的情况。
综上所述:可得状态转移方程式:
所以,可以写出代码:
int dp[N][M];
for (int i=1;i<=n;++i){
for (int j=0;j<=m;++j){
if (j < v[i]) dp[i][j] = dp[i-1][j];
else dp[i][j] = max(dp[i-1][j],dp[i][j-v[i]]+w[i]);
}
}
我们对代码进行优化,得到以下代码:
int dp[M];
for (int i=1;i<=n;++i){
for (int j=v[i];j<=m;++j){ // 正序!!!
dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
}
}
可以发现,这段代码与01背包唯一的不同只有第
根据完全背包的定义,每一种物品都可以无限地取,因此如果要选择,我们应该考虑的是前
这样,完全背包的状态转移方程式:(一定要注意,这里是正序!)
模板题
P1616 疯狂的采药
这道题需要注意两个点:
- 这道题目的
n 和m ,依旧是反着输入的! - 需要使用 long long 类型,否则会爆掉!(代码中使用的是宏定义的形式)
正确代码:
#include <bits/stdc++.h>
#define int long long
#define N 10005
#define M 10000005
using namespace std;
int n,m;
int w[N],v[N];
int [M];
signed main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
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=v[i];j<=m;++j){
[j] = max([j],[j-v[i]]+w[i]);
}
}
cout << [m];
return 0;
}
推荐题目:
- P2918 Buying Hay S(完全背包小变形)
- P5662 纪念品
- P5020 货币系统
多重背包
多重背包问题是介于 01 背包和完全背包之间的一种问题。它的主要特点是:每种物品有固定的数量限制。
经典问题如下:
有
方法1:多重背包转 01 背包
由于多重背包问题中,每种物品的数量是固定的,所以,我们可以通过遍历这些数量以求出答案。代码如下:
int dp[M];
for (int i=1;i<=n;++i){
for (int k=1;k<=l[i];++k){// 枚举数量限制
for (int j=m;j>=v[i];--j){
dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
}
}
}
这段代码得时间复杂度为
方法2:二进制优化
为了优化,我们需要用到一个数学结论。首先,先看一下这个结论的简化版:
从
这个结论很容易得到,可以用二进制的方法来证明:因为所有数字都为
因此,对于在
设
因此,我们可以枚举
代码如下:
int dp[M];
for (int i=1;i<=n;++i){
int res = l[i];
for (int k=1;k<=res;res-=k,k*=2) // 二进制拆分
for (int j=m;j>=v[i]*k;--j) // 01背包
dp[j] = max(dp[j],dp[j-v[i]*k] + w[i] * k);
for (int j=m;j>=v[i]*res;--j) // 处理剩余部分
dp[j] = max(dp[j],dp[j-v[i]*res] + w[i]*res);
}
时间复杂度为
模板题
P1776 宝物筛选 - 洛谷
正确代码:
#include <bits/stdc++.h>
#define int long long
#define N 105
#define M 40005
using namespace std;
int n,m;
int dp[M];
int v[N],w[N],l[N];
signed main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> m;
for (int i=1;i<=n;++i) cin >> w[i] >> v[i] >> l[i];
for (int i=1;i<=n;++i){
int res = l[i];
for (int k=1;k<=res;res-=k,k*=2)
for (int j=m;j>=v[i]*k;--j)
dp[j] = max(dp[j],dp[j-v[i]*k] + w[i]*k);
for (int j=m;j>=v[i]*res;--j)
dp[j] = max(dp[j],dp[j-v[i]*res] + w[i]*res);
}
cout << dp[m];
return 0;
}
推荐题目:
- P1833 樱花(完全背包和多重背包简单结合)
分组背包
分组背包问题是背包问题的一个重要变种,它引入了物品组的概念。它的主要特点是:分组背包中的物品被划分为若干组,对于相同组物品来说,最多只能选择组内的一件物品。
经典问题如下:
有
考虑前
考虑前
- 不取第
i 组中的物品,问题就变成了考虑前i-1 组物品,总体积为j 时的情况; - 取前
i 组中的物品,枚举组中的物品k ,问题就变成了考虑前i-1 组物品,总体积为j-v_k 时的情况。
用
代码如下:
for (int i=1;i<=组数;++i){
for (int j=0;j<=m;++j){
dp[i][j] = dp[i-1][j];
for (int k : c[i])
if (v[k] <= j)
dp[i][j] = max(dp[i][j],dp[i-1][j-v[k]]+w[k]);
}
}
接下来,使用滚动数组进行优化,得到代码:
int dp[M];
for (int i=1;i<=组数;++i)
for (int j=m;j;--j)
for (int k : c[i]) // 枚举每个组中的物品
if (v[k] <= j)
dp[j] = max(dp[j],dp[j-v[k]]+w[k]);
时间复杂度为
模板题
P1757 通天之分组背包 - 洛谷
正确代码:
#include <bits/stdc++.h>
#define ll long long
#define N 1005
#define M 1005
using namespace std;
int n,m;
int v[N],w[N],a[N];
vector<int> c[N];
int dp[M];
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> m >> n;
for (int i=1;i<=n;++i){
cin >> v[i] >> w[i] >> a[i];
c[a[i]].push_back(i);
}
for (int i=1;i<=n;++i){
for (int j=m;j;--j){
for (int k : c[i]){
if (v[k] <= j)
dp[j] = max(dp[j],dp[j-v[k]]+w[k]);
}
}
}
cout << dp[m];
return 0;
}
推荐题目:
- P1064 金明的预算方案(将依赖关系转化为分组)
二维背包
二维背包是背包问题的重要扩展,它在主要特点是:在01背包的基础上增加了第二个维度的限制。不仅要考虑背包的容量限制,还要考虑另一个资源限制(如重量、体积、时间等)。
经典问题如下:
有
这个问题其实很好思考,和01背包的做法一样,考虑前
考虑前
- 不选第
i 个物品,问题就转换成了考虑前i-1 个物品,总体积为j ,总体力为x 时的情况; - 选第
i 个物品,问题就转换成了考虑前i-1 个物品,总体积为j-v_i ,总体力为x-t_i 时的情况。
这样,就可以得到二维背包的状态转移方程式:
于是,可以得到代码:
int dp[N][M][K];
for (int i=1;i<=n;++i){
for (int j=0;j<=m;++j){
for (int x=0;x<=k;++x){
dp[i][j][x] = dp[i-1][j][x];
if (j >= v[i] && x >= t[i])
dp[i][j][x] = max(dp[i][j][x],dp[i-1][j-v[i]][x-t[i]]);
}
}
}
接下来,使用滚动数组进行优化,得到代码:
int dp[M][K];
for (int i=1;i<=n;++i)
for (int j=m;j>=v[i];--j) // 类似01背包
for (int x=k;x>=t[i];--x)
dp[j][x] = max(dp[j][x],dp[j-v[i]][x-t[i]]+w[i]);
时间复杂度为
模板题
P1507 NASA的食物计划
正确代码:
#include <bits/stdc++.h>
#define int long long
#define N 55
#define M 405
#define K 405
using namespace std;
int H,T,n;
int v[N],t[N],w[N];
int dp[M][K];
signed main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> H >> T >> n;
for (int i=1;i<=n;++i) cin >> v[i] >> t[i] >> w[i];
for (int i=1;i<=n;++i)
for (int j=H;j>=v[i];--j) // 类似01背包
for (int x=T;x>=t[i];--x)
dp[j][x] = max(dp[j][x],dp[j-v[i]][x-t[i]]+w[i]);
cout << dp[H][T];
return 0;
}
推荐题目:
-
P1509 找啊找啊找GF
-
P1586 四方定理
-
P2854 Cow Roller Coaster S(感觉像是二维背包又不像)
总结
背包问题是
以下为各类背包问题的对比图:
| 问题类型 | 物品特性 | 核心代码 | 时间复杂度 |
|---|---|---|---|
| 01 背包 | 每件物品选 |
逆序循环 | |
| 完全背包 | 物品无限供应 | 正序循环 | |
| 多重背包 | 每件物品最多选 |
二进制优化+01背包 | |
| 分组背包 | 组内物品互斥 | 组→容量→组内物品循环 | |
| 二维背包 | 两个维度限制 | 双重逆序循环 |
其中,二维背包又可以扩展成多维背包,有多个维度的限制,当然,时间复杂度也会增加。