概述
背包问题描述的是一共有N
件物品,有一个容积为V
的背包,每个物品有两个属性:体积v[i]
和价值w[i]
。
背包问题的问法:在背包能装下的前提下,能装的物品最大价值是多少。
而背包问题又分为01背包、完全背包、多重背包和分组背包,这些背包问题的区别是每件物品的个数不同,在后面将详细阐述这四个背包问题的区别,以及解题思路。
01背包
01背包的关键是每个物品最多只用1次,也就是说每个物品只有选和不选两种状态。
动态规划(DP)问题一般都从两种角度考虑:一是状态表示,二是状态计算。
状态表示
01背包的状态表示我们用二维数组f[i,j]
表示,这个集合的意义是:所有只考虑前i
个物品,且总体积不大于j
的所有选法。
而这个集合每次状态转移需要满足的条件是:①只从前i
个物品中选;②总体积<=j
。集合f[i,j]
存储的是集合中所有选法的最大值。
状态计算
DP问题的状态计算主要考虑的是如何划分这个集合,并且划分集合要做到不重不漏,最后通过划分的集合得到DP的状态转移方程。
在01背包问题中,我们可以将集合中的每个元素划分成**不含i
和含i
**这两个部分。
不含i
的部分,就相当于去掉物品i
的选法的最大值,可以通过f[i-1,j]
直接得出。
含i
的部分不好直接计算,但是可以通过f[i-1,j-vi]+wi
的方式间接得出。这个式子的含义是去掉物品i
的选法的最大值加上物品i
的价值。
综上,我们可以得出01背包问题的状态转移方程为:f[i,j] = Max(f[i-1,j], f[i-1,j-vi]+wi)
01背包模板题目
代码实现
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
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 = 0; j <= m; j ++) {
f[i][j] = f[i - 1][j];
if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
cout << f[m];
return 0;
}
01背包的一维优化
通过上面的具体代码实现我们发现:二维数组一直只用了f[i]
和f[i-1]
这一层。因此可以将二维数组简化成一维滚动数组。
将f
优化成一维数组后,原来的f[i][j] = f[i - 1][j];
变成f[j] = f[j];
恒等式直接删除。
剩下条件判断:if (j >= v[i]) f[j] = max(f[j], f[j - v[i]] + w[i]);
也就是说j<v[i]
的情况下都不用做任何操作。因此可以将这个条件判断放到循环中,使j
直接从v[i]
开始循环。
优化后的核心代码类似:
for (int i = 1; i <= n; i ++) {
for (int j = v[i]; j <= m; j ++) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
但是这里有一个问题是:f[j]
都是更新后的第i
层的最大值,而f[j - v[i]] + w[i]
需要的是第i - 1
层的最大值。
解决这个问题的办法是:让j
从大到小遍历,从m
递减到vi
。
因此正确的一维优化代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
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 = m; j >= v[i]; j --) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m];
return 0;
}
背包问题未完待续…