动态规划(DP)背包问题知识点总结
动态规划(DP)背包问题知识点总结
背包问题是动态规划中最经典的一类问题,也是 C++ 算法学习、GESP、CSP-J/S、NOIP 竞赛中的重要知识点。
学习背包问题,建议按照以下顺序掌握:
1
01 背包
每种物品最多选一次
2
完全背包
每种物品可以选无限次
3
多重背包
每种物品有数量限制
4
背包变形问题
分组背包、二维费用、恰好装满、方案计数等
一、背包问题的基本模型
假设有 \(n\) 件物品,一个背包的最大容量为 \(W\)。
每件物品有两个属性:
- \(w_i\):物品的重量(占用容量)。
- \(v_i\):物品的价值(获得的收益)。
目标是在不超过背包容量的情况下,使选中物品的总价值最大。
例如:
| 物品 | 重量 \(w_i\) | 价值 \(v_i\) |
|---|---|---|
| A | 1 | 15 |
| B | 3 | 20 |
| C | 4 | 30 |
背包容量 \(W=4\)。
可以选择 A、B 或 C。最优选择是 A+B,总重量为 4,总价值为 35。
二、动态规划的核心思想
1. 状态定义
最经典的一维状态定义为:
\[ dp[j]=\text{容量不超过 }j\text{ 时的最大价值} \]
例如:
dp[0]:容量为 0 时,最大价值为 0。dp[3]:容量不超过 3 时,可以获得的最大价值。dp[4]:容量不超过 4 时,可以获得的最大价值。
2. 状态转移方程
对于一件重量为 \(w_i\)、价值为 \(v_i\) 的物品:
\[ dp[j]=\max(dp[j],dp[j-w_i]+v_i) \]
前提是 \(j\geq w_i\)。
这个公式表示两种选择:
- 不选当前物品:
dp[j] - 选择当前物品:
dp[j-w[i]] + v[i]
两者取最大值。
3. 为什么需要循环顺序?
这是背包问题最关键的知识点之一。
- 01 背包:容量从大到小遍历。
- 完全背包:容量从小到大遍历。
原因是:遍历顺序决定当前物品能否被重复使用。
三、01 背包(每件物品最多选一次)
1. 特点
每件物品只有两种选择:
- 选一次。
- 不选。
不能选第二次。
2. C++ 模板
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n, W;
cin >> n >> W;
vector<int> w(n + 1), v(n + 1);
vector<int> dp(W + 1, 0);
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
for (int i = 1; i <= n; i++) {
// 从大到小,保证每件物品最多选一次
for (int j = W; j >= w[i]; j--) {
dp[j] = max(dp[j],
dp[j - w[i]] + v[i]);
}
}
cout << dp[W] << '\n';
return 0;
}
3. 为什么容量必须倒序?
假设当前物品重量为 2、价值为 3。
如果容量正序遍历,更新后的 dp[2] 可能被 dp[4] 再次使用,从而让同一件物品被重复选择。
容量倒序可以避免这种情况。
记忆口诀:01 背包,容量倒着跑。
四、完全背包(每件物品可以选无限次)
1. 特点
每种物品可以选择:
- 不选。
- 选一次。
- 选两次。
- 选很多次。
只要总重量不超过背包容量即可。
2. C++ 模板
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n, W;
cin >> n >> W;
vector<int> w(n + 1), v(n + 1);
vector<int> dp(W + 1, 0);
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
for (int i = 1; i <= n; i++) {
// 从小到大,允许重复选择当前物品
for (int j = w[i]; j <= W; j++) {
dp[j] = max(dp[j],
dp[j - w[i]] + v[i]);
}
}
cout << dp[W] << '\n';
return 0;
}
3. 为什么容量正序?
因为更新 dp[j] 时,dp[j-w[i]] 可能已经使用过当前物品,所以可以再次选择它。
记忆口诀:完全背包,容量正着跑。
五、01 背包和完全背包的对比
| 比较项 | 01 背包 | 完全背包 |
|---|---|---|
| 每种物品数量 | 最多 1 件 | 无限件 |
| 外层循环 | 遍历物品 | 遍历物品 |
| 内层循环 | 容量倒序 | 容量正序 |
| 转移方程 | 相同形式 | 相同形式 |
| 核心区别 | 防止重复使用 | 允许重复使用 |
| 口诀 | 倒序 | 正序 |
最容易记住的模板差异只有一处:
01 背包
倒序
for (int j = W;
j >= w[i]; j--)
完全背包
正序
for (int j = w[i];
j <= W; j++)
六、多重背包(每种物品数量有限)
1. 特点
每种物品有一个数量上限 \(c_i\)。
例如,某种物品有 5 件,最多只能选 5 件。
2. 基础模板:枚举数量
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n, W;
cin >> n >> W;
vector<int> dp(W + 1, 0);
for (int i = 1; i <= n; i++) {
int w, v, c;
cin >> w >> v >> c;
// 01 背包倒序更新,每件物品最多使用一次
// 对当前种类,逐件处理 c 件物品
for (int k = 1; k <= c; k++) {
for (int j = W; j >= w; j--) {
dp[j] = max(dp[j],
dp[j - w] + v);
}
}
}
cout << dp[W] << '\n';
return 0;
}
这个写法将每件物品分别作为一件 01 背包处理,适合数量较小的情况。数量很大时,可以学习二进制分组优化,把数量拆成若干组,将复杂度从与物品总数量相关降低到与数量的对数相关。
七、其他常见背包变形
| 类型 | 主要特点 | 常见应用 |
|---|---|---|
| 分组背包 | 每组最多选择一个 | 多种课程中每组选择一门 |
| 二维费用背包 | 同时限制两种资源 | 重量和体积都有上限 |
| 恰好装满背包 | 总重量必须等于 \(W\) | 凑出指定金额、指定容量 |
| 方案数背包 | 计算满足条件的组合数 | 凑硬币、组合计数 |
| 完全背包求最小值 | 最小化物品数量或成本 | 最少硬币凑金额 |
| 混合背包 | 01、完全、多重背包混合 | 多种物品有不同数量限制 |
1. 分组背包
每一组中只能选一个物品或不选。
常见写法是先枚举组,再倒序遍历容量,并在每组内部枚举可选物品。
2. 恰好装满背包
如果要求恰好装满,不能把所有状态都初始化为 0,否则不可达状态也会被当作有效状态。
通常初始化:
vector<int> dp(W + 1, -1);
dp[0] = 0;
这里假设价值非负,-1 用于表示不可达。更新前还要判断前一个状态是否可达。
如果价值可能为负数,应该使用 bool 可达数组或其他不会与合法价值冲突的标记。
3. 方案数背包
如果题目要求“有多少种组合”,通常需要将最大值转移改为计数累加,并注意题目区分组合与排列。
例如完全背包求组合数,通常是:
for (int i = 1; i <= n; i++) {
for (int j = w[i]; j <= W; j++) {
dp[j] += dp[j - w[i]];
}
}
这段代码适用于计算使用给定物品类型凑出容量的组合数,初始化和取模规则要根据具体题目要求设置。
八、背包问题的时间复杂度
| 类型 | 常见时间复杂度 | 空间复杂度 |
|---|---|---|
| 01 背包 | \(O(nW)\) | \(O(W)\) |
| 完全背包 | \(O(nW)\) | \(O(W)\) |
| 多重背包(逐件处理) | \(O(W\sum c_i)\) | \(O(W)\) |
| 多重背包(二进制优化) | \(O(W\sum \log(c_i+1))\) | \(O(W)\) |
| 二维费用背包 | \(O(nW_1W_2)\) | \(O(W_1W_2)\) |
注意:这里的 \(W\) 是背包容量,不是输入数据的位数。若 \(W\) 非常大,\(O(nW)\) 的动态规划可能无法通过,需要考虑其他算法。
九、做题时如何判断背包类型?
背包题型判断流程
题目是否有限制容量或总和?
每种物品最多能选几次?
最多 1 次
01 背包
容量倒序
无限次
完全背包
容量正序
有限件数
多重背包
数量受限
最后再判断目标:最大价值、最小成本、方案数量,还是恰好装满。
十、练习建议
建议按下面顺序练习,不要一开始就做复杂的混合背包。
- 先掌握 01 背包,能够独立写出状态定义、转移方程和倒序循环。
- 将 01 背包的内层循环改成正序,理解完全背包为什么允许重复选择。
- 学习多重背包和分组背包。
- 再做恰好装满、方案计数、二维费用等变形。
最核心的三句话:
- 01 背包:每件物品最多一次,容量倒序。
- 完全背包:每种物品无限次,容量正序。
- 多重背包:每种物品数量有限,需要限制使用次数。

浙公网安备 33010602011771号