什么是背包问题?
背包问题(Knapsack Problem)是计算机科学和运筹学中的一个经典组合优化问题。给定一组物品,每个物品都有其特定的体积(或重量)和价值,在不超过背包总容量的情况下,如何选择物品装入背包,使得背包中物品的总价值最大化。(在学习背包问题之前,笔者建议先阅读笔者写于之前的动态规划入门:从记忆化搜索到动态规划以便更容易理解背包问题)
背包问题的多种变体:
01背包问题:每种物品只能选或不选(0次或者1次)
完全背包问题:每种物品可以选择无数次
多重背包问题:每种物品有数量限制
分组背包问题:物品被分成若干组,每组只能选一个物品
混合背包:以上四种背包问题混在一起
多维费用的背包问题:限制条件不只有体积,还会有其他因素(比如重量)
下面我将借助一些例题带读者深入理解各种背包问题:
01背包:
题目描述:有一个容量为 V 的背包和 N 件物品,每件物品有一个体积 v[i] 和一个价值 w[i]。要求从这 N 件物品中选择若干件放入背包,使得背包中物品的总体积不超过 V,且总价值最大。每件物品只能选择一次(即不能重复选择)。
看到这个问题,相信很多同学第一反应是利用贪心策略,但贪心有三种不同的情况,分别是:
- 尽可能选价值较大的物品
- 尽可能选提交较小的物品
- 尽可能选 w/v(价值 / 体积)较大的物品
但这三种策略都是可以证明为否的,且很容易举出反例,因此贪心策略不可行。
这时我们就可以考虑利用动态规划来解决这个问题,背包问题究其本质也是动态规划的一种,因此,我们可以用动态规划同款的方式来解决问题:
在体积不超过 j 的情况下,从[1, i]区间挑选物品,我们可以选择是否选择第 i 号物品,如果不选择,那么它的结果与从[1, i - 1]区间挑选物品结果一致,即f[i][j] = f[i - 1][j]
,如果选择第 i 个元素,那么之前位置所选的物品体积总合不能超过 j - v[i],即,f[i][j] = w[i] + f[i-1][j - v[i]]
。
算法原理:
- 状态表示:f[i][j]表示:在 [1,i] 区间挑选物品,总体积不能超过 j,所有选法下,最大的价值
- 状态转移方程:
f[i][j] = max(f[i - 1][j], f[i-1][j - v[i]] + w[i])(如果j-v[i] >= 0)
- 初始化:将第一行全部初始化为0
- 填表顺序:从上往下每一行
- 最终结果:f[n][v]
代码实现:
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[n][m] << endl;
return 0;
}
空间优化:可以将 f 数组从二维降为一维。由于每次更新时都是顺序处理,且使用过的旧值不会被重复利用,因此可以不断覆盖原数组,仅保留一维数组来记录最终结果。可以将 f 数组从二维降为一维。由于每次更新时都是从右往左顺序处理,且使用过的旧值不会被重复利用,因此可以不断覆盖原数组,仅保留一维数组来记录最终结果。
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] << endl;
return 0;
}
感觉看明白了的可以移步洛谷P1048采药自行检测。
完全背包:
题目描述:给定一个容量为 V 的背包和 N 种物品,每种物品有无限个可用。第 i 种物品的体积为 v[i],价值为 w[i]。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出这个最大总价值。
同样的,贪心策略同样易证为伪,这里便不再赘述,此时,其他量皆与01背包相同,所以这里主要推导状态转移方程。
在总体积不超过 j 的情况下,从[1, i]区间挑选物品,我们可以选择是否选择第 i 号物品,以及选择几个 i 号物品,当不选择第 i 号物品时 f[i][j] = f[i - 1][j]
,与前 i - 1 号保持一致。当我们选择一个时 f[i][j] = f[i-1][j-v[i]] + w[i]
,选择两个时 f[i][j] = f[i-1][j-2 * v[i]] + 2 * w[i]
,…,依次类推,选择 k 个时 f[i][j] = f[i-1][j-k * v[i]] + k * w[i]
。所以,f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i],......,f[i-1][j-k * v[i]] + k * w[i])
。但是这样,我们依然不能很直观的看出状态转移方程是什么样子的,那我们在往下看一层,即 f[i][j-v[i]] = max(f[i-1][j-v[i]], f[i-1][j-2 * v[i]] + w[i],......,f[i-1][j-k * v[i]] + (k-1) * w[i])
。这时我们可以发现,f[i][j-v[i]]
中每一个元素加上 w[i] 就可以和 f[i][j] 中的元素对应上,因此,f[i][j] 中除了 f[i-1][j] 意外所有元素取max
即为 f[i][j-v[i]] + w[i]
。所以,我们可以明确推导出f[i][j] = max(f[i][j], f[i][j-v[i]] + w[i])
。(这种技巧不一定所有的方式都适用,但当看见元素样子差不多时可以尝试这样的方式)
算法原理:
- 状态表示:f[i][j]表示:从[1,i]区间中挑选,总体积不超过j的情况下,所有选法下,最大价值
- 状态转移方程:
f[i][j] = max(f[i][j], f[i][j-v[i]] + w[i])
- 初始化:f[0][i] = 0
- 填表顺序:从上往下,每一行从左往右
- 最终结果:f[n][v]
代码实现:
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 = 1; j <= n; j++)
{
f[i][j] = f[i - 1][j];
if (j > v[i])f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
}
cout << f[n][m] << endl;
空间优化:
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 = 1; j <= n; j++)
{
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
感觉看明白了的可以移步洛谷P1616疯狂的采药自行检测。
空间优化小结:
在空间优化下
01背包:第二层 for 循环改为从大到小 从右到左
完全背包:第二层for循环依然为从小到大 从左往右
多重背包:
题目描述:有 N 种物品和一个容量为 V 的背包。第 i 种物品最多有 x[i] 件可用,每件体积是 v,价值是 w。求解将哪些物品装入背包可使这些物品的总体积不超过背包容量,且总价值最大。
由于多重背包的状态转移方程分析部分与完全背包完全相同,因此在这里便不过多赘述。即f[i][j-v[i]] = max(f[i-1][j-v[i]], f[i-1][j-2 * v[i]] + w[i],......,f[i-1][j-k * v[i]] + (k-1) * w[i])
。由于每种物品的可用件数不一致,因此不能像我完全背包一样做优化,直接用一个状态来表示若干内容,需要一次一次挨个比对。
算法原理:
- 状态表示:f[i][j]表示:从[1,i]区间中挑选,总体积不超过j的情况下,所有选法下,最大价值
- 状态转移方程:
f[i][j] = max(f[i][j], f[i][j-v[i]] + w[i])
- 初始化:f[0][i] = 0
- 填表顺序:从上往下,每一行从左往右
- 最终结果:f[n][v]
代码实现:
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)cin >> x[i] >> w[i] >> v[i];
for (int i = 1; i <= n; i++)
for (int j = m; j >= 0; j--)
for (int k = 0; k <= x[i] && k * w[i] <= j; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k * w[i]] + k * v[i]);
cout << f[n][m] << endl;
return 0;
}
分组背包
题目描述:有 N 组物品和一个容量为 V 的背包。每组物品有若干个,同一组内的物品最多只能选一个。每件物品的体积是 v,价值是 w。求解将哪些物品装入背包可使这些物品的总体积不超过背包容量,且总价值最大。
算法原理:
- 状态表示:f[i][j]表示:从前i个小组中挑选,总重量不超过j的情况下,最大价值
- 状态转移方程:
f[i][j] = max(f[i-1][j], f[i-1][j - v[k]] + w[k])
,其中 k 是第 i 组中的物品。 - 初始化:全部为0
- 填表顺序:从上往下 每一行从左往右
- 结果:f[n][v]
代码实现
int main() {
int n, V;
cin >> n >> V;
vector<vector<pair<int, int>>> groups(n + 1); // 每组物品的v和w
vector<vector<int>> f(n + 1, vector<int>(V + 1, 0));
for (int i = 1; i <= n; i++) {
int s;
cin >> s; // 第i组的物品数量
while (s--) {
int v, w;
cin >> v >> w;
groups[i].push_back({v, w});
}
}
for (int i = 1; i <= n; i++) {
for (int j = V; j >= 0; j--) {
f[i][j] = f[i - 1][j]; // 不选第i组的任何物品
for (auto item : groups[i]) {
if (j >= item.first) {
f[i][j] = max(f[i][j], f[i - 1][j - item.first] + item.second);
}
}
}
}
cout << f[n][V] << endl;
return 0;
}