本讲定位:背包是竞赛中出现频率最高的 DP 模型,也是”选或不选”类问题的总模板。本讲覆盖四种基本背包(01、完全、多重、分组)和方案数变形,核心目标只有一个——把循环顺序背后的道理彻底讲透,让学生以后写背包时是”推出来的”,不是”背出来的”。
前置知识:专题一(递推)、专题二(状态设计的基本感觉)。
标注 ⭐ 的小节为进阶内容。
共同的舞台:容量为 $W$ 的背包、$n$ 个物品,第 $i$ 个物品重 $w_i$、价值 $v_i$,求不超过容量时的最大总价值。区别只在每个物品能选几次:
| 类型 | 每种物品可选次数 | 本讲例题 |
|---|---|---|
| 01 背包 | $0$ 或 $1$ 次 | P1048 采药 |
| 完全背包 | 任意多次 | P1616 疯狂的采药 |
| 多重背包 | 至多 $m_i$ 次 | P1776 宝物筛选 ⭐ |
| 分组背包 | 每组至多选一件 | P1757 通天之分组背包 |
先把 01 背包彻底吃透——其余三种都是它的变奏。
题意:总时间 $T \le 1000$,$m \le 100$ 株草药,第 $i$ 株耗时 $t_i$、价值 $v_i$,每株最多采一次,求最大总价值。
样例:
T=70, m=3,草药(71,100) (69,1) (1,2)→ 答案3(采后两株,时间 $70$,价值 $1+2=3$)。
只用”考虑到第几个物品”做状态是不够的——还剩多少容量直接决定后面能装什么。所以状态必须同时记录两个量:
$$ f[i][j] = \text{只考虑前 } i \text{ 个物品、时间不超过 } j \text{ 时的最大价值} $$
对第 $i$ 个物品做”选或不选”的分类讨论:
$$ f[i][j] = \max\big(f[i-1][j],\ f[i-1][j-t_i] + v_i\big) $$
边界 $f[0][j] = 0$。答案 $f[m][T]$。复杂度 $O(mT)$。
#include<bits/stdc++.h>
using namespace std;
int t[105], v[105];
int f[105][1005]; // f[i][j]:前 i 株草药、时间不超过 j 的最大价值
int main() {
int T, m;
scanf("%d%d", &T, &m);
for (int i = 1; i <= m; i++) scanf("%d%d", &t[i], &v[i]);
for (int i = 1; i <= m; i++)
for (int j = 0; j <= T; j++) {
f[i][j] = f[i - 1][j]; // 不采第 i 株
if (j >= t[i])
f[i][j] = max(f[i][j], f[i - 1][j - t[i]] + v[i]); // 采第 i 株
}
printf("%d\n", f[m][T]);
return 0;
}
注意转移里全部是 $f[i-1][\cdot]$——每个物品只考虑一次,“选它”的来源必须是还没考虑过它的世界。这句话是理解下一节的钥匙。