开始前,会这些就够了
- 能读懂数组下标和
max(a,b) - 先读 场次最多与收入最高的区别
- Python 3.10+;只使用标准库
- 先读 递归如何保存尚未完成的工作:帮助理解状态之间有先后依赖;本篇把依赖显式放进表格。
桌上只有一件物品,重量2,价值3,背包容量4。最多能拿到的价值显然是3。可是同一个更新公式,仅把容量循环从大到小改为从小到大,最后竟然写出了6。
这并非加法出了错,而是数组开始重复使用当前这件物品。把动态规划压缩成一行以后,读取哪个时间点的值,就成了循环方向必须负责的事情。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压后,在含 demo.py 的目录打开终端。命令分别回车执行。容量4、重量2、价值3的小例子:从大到小读取上一轮,从小到大可能读到这一轮刚写出的结果。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
同一行背包表,向左更新还是向右更新?
正在准备实验控件…
目标是容量不超过上限、每件物品最多取一次。正向更新演示的是允许每类物品重复取用的另一种库存规则;所有重量为正整数,价值为非负整数。
每一格表示不超过这个容量时能拿到的最大价值
dp[c]表示总重量不超过c的最大价值,所以空背包允许出现,初始所有格都是0。这与“必须恰好装满c”不同;后者通常要把不可达状态单独标出来,不能直接沿用全零初值。
先看只有一件物品的情况:重量2、价值3、容量4。容量0、1放不下它;容量2、3、4都只能放这件一次。因此合理的整行是0、0、3、3、3。容量更大不等于物品会自动复制。
先保留两行,就能看清更新应该读谁
定义F(i,c)为只使用前i件物品、容量不超过c的最好价值。处理第i件、重量w、价值v时,不拿它得到F(i−1,c);拿它则得到F(i−1,c−w)+v。只有c≥w时第二项才存在,最终在两项之间取较大值。
这两个分支都读取“前i−1件”的结果。特别是拿当前物品之后,剩余容量也只能从更早的物品里挑,否则就会把第i件再次拿进来。MIT讲义的二维递推表达的正是这种跨物品层的依赖;下面的一行方向解释是对它的具体展开。
从大到小更新,较小容量还保留上一轮
压缩为一行后,公式写成dp[c]=max(dp[c],dp[c−w]+v)。如果c从4往2走,更新dp[4]时,dp[2]还没有处理当前物品,所以读到0,写成3。之后dp[3]也写成3,最后dp[2]写成3。
由于w>0,c−w总比c小。下降遍历确保读取的较小下标还没被当前这一轮覆盖,于是它仍对应上一件物品处理完后的那一行。把行号省掉了,但依赖的时间顺序没有省掉。
从小到大更新,较小容量已经可能包含当前物品
若从2往4走,先写dp[2]=3。等走到dp[4],再读dp[2]时它已变成3,于是写出3+3=6。同一件物品的价值沿较小容量格被再次带入,库存规则已经改变。
在本例的正重量、非负价值、允许空选条件下,正向更新正好对应每类物品可无限次使用的背包。它不是在所有问题里都错误,而是解了另一种数量约束。某些输入两种规则得到相同数值,也不能据此认定方向无关紧要。
两件重量相同的真实物品仍可以是两件不同物品:0/1限制针对每个输入位置最多一次,不能先按重量去重。本文不处理零重量;在无限数量模型里,零重量且正价值会让最大价值没有有限上界。
省掉一行,不代表任何演示程序都只占一行内存
不保存轨迹的标准一行更新需要O(nC)时间、O(C)工作空间,n为件数、C为整数容量。C是数值本身;如果容量用很少的二进制位表示却很大,O(nC)并不是按输入位数计算的普通多项式界。
本文Python为了打印和检查,还记录每次更新,轨迹额外占O(nC)空间。浏览器为了前后翻步保存整行快照,轨迹空间可到O(nC²)。这两者是教学记录开销,不能拿演示实现直接宣称总空间只有O(C)。容量和物品数都做了小范围限制。
完整程序:手算之外,再让独立方法核对
保存下面全部代码为 demo.py,执行 python demo.py。本次在 Windows / CPython 3.14.0 实跑,只使用标准库;同时通过 Python 3.10 语法解析,未在 Python 3.10 解释器中运行。
枚举长度0到3的物品序列,重量取1到3、价值取0到3,再枚举容量0到8,共16965组。下降更新对照全部物品子集;上升更新对照各类物品取用次数的独立枚举。另拒绝8类无效输入。
输出来自本次真实执行。检查使用显式 AssertionError,python -O 不会把检查删掉。小范围穷举支持实现核对,算法的一般理由仍由前面的依赖关系说明。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""One storage row, two loop directions, two different inventory rules."""
from itertools import product
def require(ok, message):
if not ok:
raise AssertionError(message)
def validate(items, capacity):
if type(capacity) is not int or not 0 <= capacity <= 30:
raise ValueError("integer capacity in 0..30 required")
if not isinstance(items, (list, tuple)) or len(items) > 12:
raise ValueError("at most 12 items required")
for item in items:
if not isinstance(item, (list, tuple)) or len(item) != 2:
raise ValueError("each item is (weight, value)")
weight, value = item
if type(weight) is not int or not 1 <= weight <= 30:
raise ValueError("positive integer weight in 1..30 required")
if type(value) is not int or not 0 <= value <= 100:
raise ValueError("integer value in 0..100 required")
def knapsack(items, capacity, ascending=False):
validate(items, capacity)
if type(ascending) is not bool:
raise ValueError("ascending must be bool")
dp = [0] * (capacity + 1)
trace = []
for index, (weight, value) in enumerate(items):
positions = range(weight, capacity + 1) if ascending else range(capacity, weight - 1, -1)
for c in positions:
read = dp[c - weight]
before = dp[c]
dp[c] = max(before, read + value)
trace.append((index, c, c - weight, read, before, dp[c]))
return dp, trace
def subset_reference(items, capacity):
best = 0
for mask in range(1 << len(items)):
chosen = [item for i, item in enumerate(items) if mask & (1 << i)]
if sum(weight for weight, _ in chosen) <= capacity:
best = max(best, sum(value for _, value in chosen))
return best
def unlimited_reference(items, capacity, index=0):
# Independent enumeration of the number of copies, without a DP array.
if index == len(items):
return 0
weight, value = items[index]
return max(copies * value + unlimited_reference(items, capacity - copies * weight, index + 1)
for copies in range(capacity // weight + 1))
def main():
items, capacity = [(2, 3)], 4
descending, _ = knapsack(items, capacity)
ascending, steps = knapsack(items, capacity, True)
print(f"one_item={items}; capacity={capacity}")
print(f"descending={descending}; answer={descending[-1]}")
print(f"ascending={ascending}; answer={ascending[-1]}")
print(f"ascending_last_update: capacity={steps[-1][1]}; reads_dp[{steps[-1][2]}]={steps[-1][3]}")
require((descending[-1], ascending[-1]) == (3, 6), "reuse witness")
count = 0
alphabet = tuple(product(range(1, 4), range(4)))
for size in range(4):
for items in product(alphabet, repeat=size):
for capacity in range(9):
once, _ = knapsack(items, capacity)
reusable, _ = knapsack(items, capacity, True)
require(once[-1] == subset_reference(items, capacity), "0/1 subset oracle")
require(reusable[-1] == unlimited_reference(items, capacity), "unlimited copy oracle")
count += 1
invalid = [([(0, 3)], 4), ([(2, -1)], 4), ([(True, 3)], 4), ([(2, 3)], True),
([(2, 3)], -1), ([(2, 3)], 31), ([(2, 3.5)], 4), ([(2,)], 4)]
for items, capacity in invalid:
try:
knapsack(items, capacity)
except ValueError:
pass
else:
raise AssertionError("invalid input accepted")
print(f"cases={count}; independent_oracles=subsets+copy_counts; invalid_inputs={len(invalid)}")
print("PASS: descending uses each item once; ascending allows repeated copies")
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-26 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。容量4、重量2、价值3的小例子:从大到小读取上一轮,从小到大可能读到这一轮刚写出的结果。
运行后对照结果
one_item=[(2, 3)]; capacity=4
descending=[0, 0, 3, 3, 3]; answer=3
ascending=[0, 0, 3, 3, 6]; answer=6
ascending_last_update: capacity=4; reads_dp[2]=3
cases=16965; independent_oracles=subsets+copy_counts; invalid_inputs=8
PASS: descending uses each item once; ascending allows repeated copies把每次读取标成旧行还是新行
练习不预填结果:使用两件物品(重量3,价值4)、(重量4,价值6),容量8。各画一张下降与上升更新表,在每次读取旁标出“上一轮的值”还是“本轮已经改过的值”。
然后用所有子集核对每件最多一次的答案。再改为允许重复取用,列出每类物品取几件。最后尝试一个两种规则碰巧相同的容量,说明为什么相同数值不能替代依赖分析。
记住这些要点
- 0
/1背包的拿取分支应读取上一件物品处理完后的状态。 - 一行数组下降更新保留旧依赖,上升更新允许复用当前物品。
- 同一个公式可以因读取顺序不同而对应不同库存规则。
- 不超过容量和恰好装满容量,初始状态不能混用。
别把例子用过头
- 只背“背包要倒着写”,却不检查所读取状态属于哪一轮。
- 把两个方向偶尔相同的输出当作算法等价。
- 把轨迹演示的总空间说成标准核心更新的
O(C)。
自己试一小步
容量8、物品(3,4)和(4,6)时,如何从读取顺序判断当前物品是否被重复使用?
- 分别手写两个方向的容量更新。
- 给每次读取标记其所属物品轮次。
- 用子集与取用次数两种独立方法核对。
做到这些,就算完成
- 先声明每件一次还是每类可重复。
- 容量含义是不超过上限。
- 练习不提前给出最终价值。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- MIT 6.006 Lecture 20:Knapsack 与伪多项式复杂度 (新标签页)↗
Knapsack二维物品层递推、整数容量下
O(nS)时间,以及容量数值和输入位数的区别;一行循环方向由本文依照递推展开。