前言:选还是不选?动态规划的起点
你去登山,背包容量有限。面前有 件物品,每件有重量和价值。你当然想带价值尽量多的东西,但背包装不下所有。每件物品只有一件——要么带,要么不带。
这就是 0-1 背包问题。它是动态规划(DP)最经典的入门题。
暴力做法:枚举所有 种选择方案,找出最优的。 就有 种方案, 就有 种——不可能。
DP 的思路完全不同。与其枚举”具体选了哪些物品”,不如只关心两个信息:
- 考虑了前几个物品?
- 用了多少容量?
只要知道”考虑前 个物品、用了容量 时的最大价值”,就能推导出考虑第 个物品后的结果——第 个物品要么选(占用容量、获得价值),要么不选(什么都不变),取两者较优。
这个”状态 + 转移”的思想是 DP 的核心。一旦掌握,你会发现它不仅能解决背包,还能解决一大类优化问题。这篇笔记会从状态定义开始,推导出转移方程,然后给出 C++ 实现,最后通过一道例题巩固。
0-1 背包问题
问题的本质
有 个物品,每个物品有重量 和价值 。背包容量为 。每个物品要么选要么不选(0-1),求能装入的最大总价值。
暴力枚举所有 种组合, 就有 种——不可行。
关键洞察:不需要记住”选了哪些物品”,只需要记住”用多大容量、考虑了几个物品时的最大价值”。这是一个状态,而状态之间有明确的递推关系——这就是动态规划。
状态定义
:考虑当前物品时,容量为 能获得的最大价值。
状态转移
对每个物品 ,枚举容量 :
- 不选:价值不变, 维持原值
- 选:占用 的容量,获得 的价值,即
取两者较优:
关键细节:内层循环 必须从大到小枚举。因为每个物品只能选一次,如果从小到大枚举, 可能已经被当前物品更新过,等于同一个物品选了两次。从大到小枚举保证 是上一轮(还没考虑当前物品)的值。
实现
int dp[MAXN]; // dp[j] = 容量 j 时的最大价值
int knapsack(int n, int W, int w[], int v[]) {
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]);
return dp[W];
}
空间只需 ——滚动数组,每轮复用同一个一维数组。
复杂度
时间 ,空间 。
注意:这是伪多项式时间—— 是数值大小而非输入长度,所以严格来说不是多项式算法。但对 级别的实际问题完全够用。
变体速查
| 变体 | 区别 | 改动 |
|---|---|---|
| 完全背包 | 每个物品可选无限次 | 从小到大枚举 |
| 多重背包 | 每个物品有数量上限 | 二进制拆分或单调队列优化 |
| 分组背包 | 每组至多选一个 | 外层加一组循环,组内互斥 |
例题:采药
有 分钟时间和 株草药,每株草药需要时间 、价值 。每株只能采一次,求能采到的最大总价值。
,
标准的 0-1 背包,时间就是容量。
#include <cstdio>
#include <algorithm>
using namespace std;
int dp[1001];
int main() {
int T, n;
scanf("%d%d", &T, &n);
for (int i = 0; i < n; i++) {
int t, v;
scanf("%d%d", &t, &v);
for (int j = T; j >= t; j--)
dp[j] = max(dp[j], dp[j - t] + v);
}
printf("%d\n", dp[T]);
return 0;
}
解析:每株草药就是一个物品,时间 是重量, 是背包容量。内层从 到 倒序枚举,确保每株草药只被选一次。最终 dp[T] 就是用满全部时间能获得的最大价值。不需要二维数组——因为转移只依赖上一轮的值,一维滚动即可。