01 背包问题:C++ 一维数组与二维数组
01 背包问题:C++ 一维数组与二维数组
01 背包是动态规划(DP)的经典问题。下面通过同一道题,分别讲解 二维数组写法和一维数组优化写法。
一、题目示例
有 3 件物品,背包容量为 4。每件物品有重量和价值,每件物品最多选择一次。
| 物品 | 重量 w |
价值 v |
|---|---|---|
| 物品 1 | 1 | 15 |
| 物品 2 | 3 | 20 |
| 物品 3 | 4 | 30 |
求背包最多能装入的最大价值。
最优方案:选择物品 1 和物品 2,总重量为 4,总价值为 35。
二、二维数组写法(基础版)
1. 状态定义
dp[i][j]
表示:只考虑前 i 件物品,背包容量为 j 时,能够获得的最大价值。
状态转移分为两种情况:
- 不选第
i件物品:dp[i-1][j] - 选择第
i件物品:dp[i-1][j-w[i]] + v[i]
因此:
\[ dp[i][j]= \begin{cases} dp[i-1][j],&j<w_i\\ \max(dp[i-1][j],dp[i-1][j-w_i]+v_i),&j\ge w_i \end{cases} \]
2. 完整 C++ 代码
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n = 3; // 物品数量
int W = 4; // 背包容量
int w[4] = {0, 1, 3, 4}; // 重量
int v[4] = {0, 15, 20, 30}; // 价值
int dp[4][5] = {0};
// i 表示正在考虑第 i 件物品
for (int i = 1; i <= n; i++) {
// j 表示背包容量
for (int j = 0; j <= W; j++) {
// 不选择第 i 件物品
dp[i][j] = dp[i - 1][j];
// 如果容量足够,可以考虑选择
if (j >= w[i]) {
dp[i][j] = max(
dp[i][j],
dp[i - 1][j - w[i]] + v[i]
);
}
}
}
cout << dp[n][W] << '\n';
return 0;
}
输出:
35
3. 二维数组如何理解?
以 dp[2][4] 为例:
它表示只考虑前两件物品,背包容量为 4 时的最大价值。
- 不选物品 2:价值为
dp[1][4] = 15。 - 选物品 2:剩余容量为 1,可以放物品 1,价值为
dp[1][1] + 20 = 35。
因此:
\[ dp[2][4]=\max(15,35)=35 \]
二维数组的优点是状态清晰,适合初学者理解状态转移过程。
三、一维数组写法(空间优化版)
二维数组的状态只依赖上一行:
\[ dp[i-1][j],\quad dp[i-1][j-w_i] \]
因此,可以使用一个一维数组保存状态,节省空间。
1. 状态定义
dp[j]
表示当前已经处理过的物品中,背包容量不超过 j 时的最大价值。
2. 完整 C++ 代码
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n = 3;
int W = 4;
int w[4] = {0, 1, 3, 4};
int v[4] = {0, 15, 20, 30};
int dp[5] = {0};
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;
}
输出:
35
3. 为什么必须倒序遍历?
这是 01 背包最重要的知识点。
假设当前物品重量为 2、价值为 3,背包容量为 4。
如果从小到大遍历:
dp[2]更新为 3。- 接着更新
dp[4]时,使用了刚更新的dp[2]。 - 相当于把同一件物品使用了两次。
这样就变成了完全背包。
因此,01 背包必须:
for (int j = W; j >= w[i]; j--)
口诀:01 背包容量倒序,避免同一件物品重复使用。
四、二维数组与一维数组对比
| 比较项目 | 二维数组 | 一维数组 |
|---|---|---|
| 状态 | dp[i][j] |
dp[j] |
| 含义 | 前 i 件物品的最优值 |
当前处理阶段的最优值 |
| 空间复杂度 | \(O(nW)\) | \(O(W)\) |
| 时间复杂度 | \(O(nW)\) | \(O(nW)\) |
| 容量遍历 | 通常正序即可 | 必须倒序 |
| 优点 | 容易理解、方便还原选择方案 | 节省内存 |
| 推荐学习顺序 | 先学 | 再学 |
这里二维数组使用上一行进行状态转移,所以容量 j 可以从小到大遍历;一维数组则必须倒序,才能保留上一轮的状态。
五、交互练习:判断循环方向
问题:下面哪段代码是正确的 01 背包一维数组写法?
A. for (int j = W; j >= w[i]; j--)
B. `for (int j = w[i]; j
查看答案
六、竞赛中应该如何选择?
- GESP、CSP-J 初学阶段: 先掌握二维数组,理解状态定义和状态转移。
- 熟悉 DP 后: 掌握一维数组优化,这是更常用的写法。
- 需要输出具体选择了哪些物品: 二维数组通常更方便回溯方案;一维数组也可以实现,但需要额外记录或设计回溯方法。
最后记住两条核心代码:
// 二维数组:使用上一行的状态
dp[i][j] = max(dp[i-1][j],
dp[i-1][j-w[i]] + v[i]);
// 一维数组:容量必须倒序
dp[j] = max(dp[j],
dp[j-w[i]] + v[i]);
两种写法解决的是同一个问题,区别在于一维数组通过倒序遍历,实现了空间优化。

浙公网安备 33010602011771号