01 背包
取或不取·一维逆推·恰好装满
本课摘要
01 背包课程回答“每件物品只能取一次时,为什么容量必须逆序更新”。内容以取或不取·一维逆推·恰好装满为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断01 背包的适用条件与状态边界
- 围绕“取或不取·一维逆推·恰好装满”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
从「整件取舍」说起
先看一个具体场景:有 3 件物品,一个容量 的背包。每件物品有自己的重量 和价值 , 而且要么整件装入、要么留下,没有「装半件」这回事(这就是「01」的含义:每件取 0 次或 1 次)。目标:在不超重的前提下,让装入的总价值最大。
第一反应也许是贪心:按「性价比」 从高到低装。这里三件的性价比是 , 于是先装物品 1(),再装物品 2(,累计 5),想装物品 3 时 塞不下,贪心只拿到 。
可最优其实是物品 2 + 物品 3:重量 ,价值 。贪心输了 2。「整件取舍」的最优,贪心按不住,因为此刻的最优选择,依赖后面还剩多少空间,是一个牵一发动全身的全局问题。
那把 件物品「取 / 不取」的所有组合都枚举一遍?那是 种, 就已是天文数字。 DP 的思路,是把这 的爆炸,压成一张逐格填写的表。
状态与转移:取,还是不取
定状态。设 表示:只在前 件物品里挑选、且总重量不超过 时,能得到的最大价值。 把「逐件考虑」当作阶段,第 阶段只决断一件事,第 件,取还是不取?
不取第 件:它没参与,前 件的最优就等于前 件在同容量 下的最优,即 。
取第 件(前提装得下 ):先腾出 的空间给它,剩下的 容量留给前 件去最优,再加上它自己的价值 ,即 。
两条路要价值最大,取较大者,就得到转移方程:
边界:(一件都不考虑,价值为 0)。答案:。
本质
这一步把「 种组合」拆成了「每件物品在参与 / 不参与两种局面下的最优」,用 张表格格子,装下了指数级的搜索。
跟着算一遍
用刚才的例子(物品 ,容量 8)走几步,把方程「跑起来」:
看它一格一格长出来
自主设计数值
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
卷成一维:滚动数组与逆推
注意转移只用到上一行 。既然如此,何必保留所有行?用一维 就地更新即可,空间从 压到 :
但方向必须逆推( 从 到 ):算 要用「上一件」留下的 ,逆推时它还没被本件动过,是干净的旧值。方向反过来会怎样?这不是小瑕疵,而是会让答案彻底跑偏,下一节把它摊开看。
为什么不能正推:一件物品被反复装入
把内层循环从逆推改成正推( 从 到 ),代码只差一个方向,结果却会错得离谱。病根就一句话:正推时,你用来更新 的 ,可能在本轮已经被同一件物品改过了。
用最干净的例子看,只有一件物品 ,容量 6:
记死:01 逆推、完全顺推
01 背包每件至多取一次,必须逆推,让 保持「上一件」留下的干净旧值;而这个「正推会重复取」的 bug,到 完全背包 里恰好翻身成想要的特性(每种无限件)。同一段转移,循环方向决定物种。
下面把两个方向并排跑给你看,改物品的 或容量:左边逆推恒等于一件的价值,右边正推随容量成倍上涨。
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
例题
#include <iostream>
#include <algorithm>
using namespace std;
int t[105], v[105];
int f[1005]; // f[j]:用时不超过 j 的最大价值
int main()
{
int T, M;
cin >> T >> M;
for (int i = 1; i <= M; i++)
cin >> t[i] >> v[i];
for (int i = 1; i <= M; i++) // 逐株草药
for (int j = T; j >= t[i]; j--) // ★逆推:从大容量往小推
f[j] = max(f[j], f[j - t[i]] + v[i]);
cout << f[T] << endl;
return 0;
}#include <iostream>
#include <algorithm>
using namespace std;
int w[3405], d[3405];
int f[12885];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> w[i] >> d[i];
for (int i = 1; i <= n; i++) // N 大,二维会 MLE,必须一维
for (int j = m; j >= w[i]; j--)
f[j] = max(f[j], f[j - w[i]] + d[i]);
cout << f[m] << endl;
return 0;
}#include <iostream>
using namespace std;
int a[105];
int f[10005]; // f[j]:恰好花 j 元的方案数
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
f[0] = 1; // 花 0 元有 1 种方案(什么都不点)
for (int i = 1; i <= n; i++)
for (int j = m; j >= a[i]; j--)
f[j] += f[j - a[i]]; // 计数:max 换成累加
cout << f[m] << endl;
return 0;
}
