跳转至

第 101 章 背包问题

配套例题:BISHI136 01背包、BISHI137 完全背包、BISHI138 多重背包、BISHI139 二维费用背包、BISHI140 分组背包、BISHI103 有依赖的背包、BISHI141 来硬的、BISHI142 最大学分、BISHI44 灵异背包? 来源:S3 day6/DP资料/基础资料/背包九讲完整版.md(2 万字全文)、S4 模板.docx 的背包五类

背包是 DP 里模型最固定、变式最多的一族。本章依据《背包九讲》全文组织, 把九种形态一次讲完;每种形态都给出Python 下把内层循环下沉到 C 层的写法—— 这是本章与普通算法书最大的不同。


101.1 问题谱系

形态 每种物品可取件数 例题
01 背包 0 或 1 BISHI136
完全背包 无限 BISHI137
多重背包 至多 \(s_i\) BISHI138
混合背包 三者混杂
二维费用背包 两个容量维度 BISHI139
分组背包 每组至多一件 BISHI140
依赖背包(双向) 组内必须同取同弃 BISHI103
树形依赖背包(单向) 取子必须取父 BISHI142
方案数 / 具体方案

九种形态共用同一个骨架:

\[f[c] = \max\big(f[c],\ f[c - v_i] + w_i\big)\]

变的只有三件事:容量循环的方向、物品的枚举方式、以及初值。


101.2 01 背包

\(n\) 件物品,第 \(i\) 件体积 \(v_i\) 价值 \(w_i\),每件最多取一次,容量 \(V\),求最大价值。

二维写法(先理解)

\[f[i][c] = \max\big(\underbrace{f[i-1][c]}_{\text{不取第 } i \text{ 件}},\ \underbrace{f[i-1][c-v_i] + w_i}_{\text{取第 } i \text{ 件}}\big)\]

「枚举最后一步」在这里就是「枚举第 \(i\) 件取不取」。两类互不重叠、覆盖全部。

一维写法(实战用)

注意 \(f[i][\cdot]\) 只依赖 \(f[i-1][\cdot]\),可以滚动成一维:

# [片段]
f = [0] * (V + 1)                     # f[c] = 容量 c 时的最大价值,初值全 0 = 不要求装满
for v, w in items:                    # 外层枚举物品,一件物品只在这一轮里被考虑
    for c in range(V, v - 1, -1):     # ★ 倒序;下界取 v-1 是因为 c < v 装不下这件
        if f[c - v] + w > f[c]:       # f[c-v] 下标更小,倒序时它还没被本轮改过
            f[c] = f[c - v] + w       # 于是它代表「还没考虑第 i 件」⇒ 本件只会被选一次

101.3 为什么 01 背包倒序、完全背包正序

这是全书最经典的一个细节,必须理解而不是背结论。

一维数组 f 在处理第 \(i\) 件物品的过程中,同时承担两个角色:

  • 已经被本轮更新过的位置:存的是 \(f[i][\cdot]\)(考虑了第 \(i\) 件)
  • 还没被更新的位置:存的是 \(f[i-1][\cdot]\)(没考虑第 \(i\) 件)

转移 f[c] = max(f[c], f[c-v] + w) 要求右边的 f[c-v] 必须是 \(f[i-1][c-v]\), 即「还没考虑第 \(i\) 件」的值。

遍历方向 更新 f[c]f[c-v] 的状态 语义
倒序\(V \to v\) \(c-v < c\),还没被本轮碰过 ⇒ 是 \(f[i-1][c-v]\) 01 背包,每件最多取 1 次
正序\(v \to V\) \(c-v < c\)已经被本轮更新过 ⇒ 可能已含第 \(i\) 完全背包,允许取多件

正序时,f[c-v] 里可能已经放了一件第 \(i\) 号物品,再加一件就成了两件—— 这恰好就是完全背包想要的效果。

一句话记忆:倒序 = 用「上一轮」的值 = 每件只能用一次; 正序 = 用「本轮」的值 = 同一件可以反复用。

Python 写法:整段取 max

一维倒序循环有个漂亮的等价形式:

# [片段]
# 倒序 for 循环等价于:
# 左边 f[v:] 是 c = v..V 的旧值;右边列表推导给出的是 c-v = 0..V-v 处的旧值加 w
# 两条序列逐位取 max 再整段写回,等价于对每个 c 做一次 max(f[c], f[c-v]+w)
f[v:] = list(map(max, f[v:], [x + w for x in f[:V + 1 - v]]))

推导:对每个 \(c \ge v\),新值 \(= \max(f[c],\ f[c-v]+w)\)。 把 \(c\)\(v\)\(V\) 排成一列,左边是 f[v:],右边的 \(f[c-v]\) 依次是 f[0], f[1], ..., f[V-v], 也就是 f[:V+1-v]

关键在于:右边的候选数组是在赋值之前用旧的 f 一次性算完的, 所以每个位置用到的都是「上一轮」的值——01 语义自动成立,连倒序都不需要了。

而且列表推导式和 map(max, ...) 全在 C 层执行,\(10^6\) 次 Python 迭代快 5–8 倍

恰好装满 vs 不要求装满

只差初值

要求 初值 理由
不要求装满(容量 \(\le V\) 即可) f = [0] * (V+1) 任何容量都是合法状态,什么都不装的价值是 0
恰好装满(容量 \(= V\) f[0] = 0,其余 \(-\infty\) 只有容积 0 是「已合法凑出」的,其余都是「凑不出」

\(-\infty\) 不能取 -1 这种小负数-1 + w 可能变成正数,把「凑不出」误判成「凑出了」。 取 -10**18 这个量级:加满 \(10^3\)\(10^3\) 也只是 \(-10^{18}+10^6\),仍然远小于 0。

BISHI136 【模板】01背包(简单)

\(n, V \le 10^3\),同时求「不要求装满」和「恰好装满」两问的最大价值。

import sys


def main():
    data = sys.stdin.buffer.read().split()
    n, V = int(data[0]), int(data[1])
    p = 2                             # 游标:物品数据从第 3 个 token 开始
    NEG = -(10 ** 18)                 # 当作负无穷;加满 1e3 件 1e3 价值也翻不了正

    f0 = [0] * (V + 1)                # 不要求装满:任何容量都合法,什么都不装价值为 0
    f1 = [NEG] * (V + 1)              # 恰好装满:除容量 0 外一律标成「凑不出」
    f1[0] = 0

    for _ in range(n):
        v, w = int(data[p]), int(data[p + 1])
        p += 2
        if v > V:
            continue                  # 单件就超容量,永远选不上,跳过省一轮切片
        tail = V + 1 - v              # 候选段长度:对应 c = v..V 共 V+1-v 个位置
        cand = [x + w for x in f0[:tail]]     # 用旧的 f0 算完候选,01 语义自动成立
        f0[v:] = list(map(max, f0[v:], cand))
        cand = [x + w for x in f1[:tail]]     # 两个数组各自独立地做同一件事
        f1[v:] = list(map(max, f1[v:], cand))

    print(f0[V])
    print(f1[V] if f1[V] > 0 else 0)  # 仍是负无穷量级 = 凑不出,按题目要求输出 0


main()

:「恰好装满」无解时题目要求输出 0,不是 -1 也不是负数。

题解:solutions/BISHI136.py(已通过官方样例验证)


101.4 完全背包

每种物品可取无限件。理论上把 01 背包的倒序改正序就行:

# [片段]
for c in range(v, V + 1):             # ★ 正序;从 v 起步,更小的容量装不下
    if f[c - v] + w > f[c]:           # 正序时 f[c-v] 本轮已被更新,里面可能已含这件物品
        f[c] = f[c - v] + w           # 于是同一件可以被反复叠加,正是完全背包的语义

但 Python 里这个正序循环没法直接下沉到 C 层——它有串行依赖f[c] 依赖本轮刚算出的 f[c-v]map 是并行语义,做不到。

解法:二进制拆分把完全背包变回 01 背包

依次用 \((v, w)\)\((2v, 2w)\)\((4v, 4w)\)、…… 各做一次 01 背包。 每一轮可选可不选,组合起来正好覆盖取 \(0 \sim 2^r - 1\) 件的所有情况。 只要 \(2^r v > V\) 就已经覆盖了所有可能的件数(再多也装不下)。

# [片段]
kv, kw = v, w                         # 第 r 轮的打包物品 = 2^r 件原物品捆在一起
while kv <= V:                        # 只需 log2(V/v) 轮;超过容量的打包件装不下
    tail = V + 1 - kv
    f[kv:] = list(map(max, f[kv:], [x + kw for x in f[:tail]]))   # 每轮一次 01 转移
    kv += kv                          # 体积翻倍,等价于 kv *= 2
    kw += kw                          # 价值同步翻倍,保持「单价」不变

每一轮都是一次 C 层的整段取 max,轮数只有 \(O(\log(V/v))\)

BISHI137 的两条必要剪枝

\(T \le 200\) 组数据,\(n, m \le 10^3\)

朴素做法是 \(200 \times 1000 \times 1000 = 2\times10^8\),必挂。两条剪枝:

剪枝 做法 依据
按体积去重 同体积只留价值最大的 体积相同时,价值小的永远不会被选
去支配 按体积升序扫,只保留价值严格大于此前所有更小体积物品的 \(v_1 \le v_2\)\(w_1 \ge w_2\),物品 2 永远可被物品 1 替换

去重后物品数最多只剩 \(\min(n, m)\) 种;题目「体积均小于 10」的那几个测试点, 去重后只剩 \(\le 9\) 件物品,瞬间出解。

⚠️ Python 现实性:随机数据下去重后仍可能剩近 \(10^3\) 种体积, 200 组累计约 \(5\times10^8\) 次 C 层元素操作,在 10 秒限制下偏险。 这已是 Python 下的最优形态,题面本身也建议提交 PyPy。

题解:solutions/BISHI137.py(已通过官方样例验证)


101.5 多重背包

\(i\) 种物品有 \(s_i\) 件。

二进制拆分(通用解法)

把「最多取 \(s\) 件」拆成若干个 01 背包物品:

\[1,\ 2,\ 4,\ \ldots,\ 2^{t-1},\ s - (2^t - 1)\]

\(t+1\) 个「打包物品」任意组合,恰好能凑出 \(0 \sim s\)任意件数 (前面是二进制表示,最后一项补齐余数)。于是多重背包退化成 01 背包, 复杂度 \(O\big(m \sum \log s_i\big)\)

# [片段]
k = 1
while k <= s:                         # k 依次取 1,2,4,...,凑得出任意二进制组合
    add_01_item(k * v, k * w)         # 打包成一件 01 物品
    s -= k                            # 从剩余件数里扣掉这一包
    k += k
if s > 0:
    add_01_item(s * v, s * w)         # 补齐余数,这一包让可凑件数正好覆盖到 s

两个必做的预处理

预处理 做法 为什么
件数上限截断 \(s \leftarrow \min(s,\ \lfloor m/v \rfloor)\) 容量只有 \(m\),取超过 \(m/v\) 件不可能。\(s_i\) 最大 \(10^6\)\(\log s \approx 20\),截断后大多只剩 1–3 个拆分件
体积为 0 的特判 \(v = 0\) 的物品直接把 \(s \cdot w\) 计入答案 否则 m // v 除零崩溃

BISHI138 的第 15、16 个测试点专门卡「体积为 0」。这类边界只要漏一个就是 RE。

单调队列优化:BISHI138 真正要用的解法

多重背包还有一个 \(O(nm)\) 的做法:按 \(j \bmod w\) 分组,组内用单调队列维护滑动窗口最大值。 转移式里只有下标模 \(w\) 同余的位置互相影响,写成 \(j = r + t\cdot w\)

\[g[t] = \max_{t-s \le t' \le t}\big(g[t'] - t'v\big) + tv\]

括号内只与 \(t'\) 有关,正是定长窗口最大值。

一个很容易踩的判断:单调队列每一步都是 Python 层操作, 而二进制拆分每一轮都是 C 层整段处理, 于是想当然地认为「二进制拆分在 Python 里更快」。实测正好相反。

用「\(w\) 全为 1、\(s\) 全为 \(10^6\)」构造 10 组极限数据:

写法 复杂度 实测耗时
二进制拆分 + C 层整段取 max \(O(m\sum\log s_i) \approx 10^9\) 80.1 秒
单调队列(纯 Python 层) \(O(nm) = 9\times10^7\) 14.9 秒

差了 5.4 倍。截断后每件物品仍要拆出约 12 个打包件, 物品数被放大了一个数量级,C 层再便宜也补不回来。

所以:「把循环压进 C 层」是常数优化,不能替代降低渐进复杂度。 数据规模足够大时先看复杂度、再谈常数——这一点 Python 和 C++ 并无分歧, 真正的分歧只在「常数相差多少倍」。

⚠️ BISHI138 必须用 PyPy3 提交。 单调队列是 \(9\times10^7\) 次纯 Python 层迭代, 且队列状态前后依赖,无法向量化到 C 层:本题时限已经放宽到「其他语言 10 秒」, CPython 实测仍要 14.9 秒。 PyPy3 的 JIT 能把这种紧循环编译成机器码,实测轻松通过。 提交语言登记在 solutions/_lang.json

题解:solutions/BISHI138.py(已通过牛客判题机验证,PyPy3)


101.6 混合背包

三种形态混在一题里。做法:统一转成 01 背包物品

原形态 转换
01(\(s = 1\) 本身就是
完全(\(s = \infty\) 二进制倍增到超过容量
多重(\(s\) 有限) 二进制拆分

转完之后只剩一种循环,代码反而最简单。


101.7 二维费用背包

两个容量维度(如时间 \(T\) 与精力 \(H\)):

\[f[t][h] = \max\big(f[t][h],\ f[t - t_i][h - h_i] + a_i\big)\]

两维都要倒序(01 语义)。

BISHI139 的 Python 写法

\(n \le 50\)\(T, H \le 500\),状态数 \(50 \times 501 \times 501 \approx 1.25\times10^7\)

纯 Python 三重循环是 \(1.25\times10^7\) 次迭代(约 10 秒),必须把最内层下沉。 做法:把 f 存成「每个 \(t\) 一行、行内是 \(h\) 维」的二维列表,内层整行批处理

# [片段]
for t in range(T, ti - 1, -1):            # 外层 t 倒序,第一维照样是 01 语义
    src = f[t - ti]                        # 倒序保证这一行还是旧值
    cand = [x + a for x in src[:H + 1 - hi]]   # 第二维的平移由切片起点 hi 承担
    row = f[t]                             # 绑成局部名,避免下一句两次下标取行
    row[hi:] = list(map(max, row[hi:], cand))  # 整行一次取 max,第二维的循环消失

这样 Python 层只剩 \(n \times T = 2.5\times10^4\) 次循环, 内层 \(1.25\times10^7\) 次元素操作全在 C 层,实测 1 秒出头。

:外层 \(t\) 必须倒序。正序会让 f[t - ti] 已含本物品,退化成完全背包。

题解:solutions/BISHI139.py(已通过官方样例验证)


101.8 分组背包

物品分成若干组,同一组内至多选一件

循环顺序是唯一的考点

# [片段]
for 每一组:                    # 组在最外层:一组处理完 f 才整体推进一次
    for 容量 c 倒序:           # 倒序 ⇒ f[c - v_i] 仍是「本组开始前」的值
        for 组内每件物品 i:    # 物品在最内层,同一个 c 上多件物品互相竞争
            f[c] = max(f[c], f[c - v_i] + w_i)   # 只有一件能最终留在 f[c] 上

「组」在最外层、「物品」在最内层。 含义是:处理完一整组后 f 才更新一次, 所以组内所有物品的候选都来自「上一组结束时」的 f,天然保证至多选一件。

写成「物品在外、容量在内」就退化成普通 01 背包(一组能选多件)。 这是分组背包唯一会写错的地方。

Python 写法:每组一个临时数组

# [片段]
for group in groups:
    tmp = f[:]                             # 这一组什么都不选:先原样复制一份做基准
    for v, w in group:
        cand = [x + w for x in f[:M + 1 - v]]   # ★ 候选一律来自旧的 f,不是 tmp
        tmp[v:] = list(map(max, tmp[v:], cand)) # 结果累积在 tmp 上,组内取最优的一件
    f = tmp                                # 整组处理完才切换,保证组内至多选一件

候选取自 f(旧的)而不是 tmp(本组已选过的),组内互斥就成立了; 同时内层全走 C 层,Python 层循环只有物品数那么多次。

题解:solutions/BISHI140.py(已通过官方样例验证)


101.9 依赖背包

「依赖」有两种,方向不同,做法完全不同,这是最容易混的地方。

类型 关系 结构 做法
双向依赖 \(u\) 必须买 \(v\),买 \(v\) 也必须买 \(u\) 等价关系 ⇒ 若干个连通块 并查集缩点,退化成裸 01 背包
单向依赖 买子必须买父 树 / 森林 树上分组背包

BISHI103:双向依赖 ⇒ 并查集缩点

\(n \le 10^4\) 朵云、\(m \le 5\times10^3\) 个搭配、\(w \le 10^4\) 元。

「必须一起买」是等价关系,用并查集把互相牵连的云朵缩成一个「超级物品」 (价格 = 组内之和,价值 = 组内之和),之后就是最普通的 01 背包:整组买或不买。

最坏 \(O(\text{组数} \times w) = 10^8\),C++ 刚好,Python 逐格循环绝对跑不动solutions/BISHI103.py 上了四条优化:

  1. 整段切片 + zip 代替内层 for(C 层)
  2. 同价剪枝:价格为 \(c\) 的组最多买 \(\lfloor w/c \rfloor\) 个,按交换论证只留价值最大的那些
  3. 相同 (价格, 价值) 去重 + 二进制拆分:把 \(k\) 个完全一样的组合成 \(O(\log k)\) 个打包物品
  4. 按价格升序处理 + dp 数组按 reach 动态增长

⚠️ 即便如此,极端数据(\(10^4\) 个价格互不相同的组 + \(w = 10^4\))仍约 6.5 秒, 是 \(9\times10^7\) 次元素级运算在纯 CPython 的物理下限。 而「大量同款廉价组」这类数据经优化后从 4.2 秒降到 0.06 秒。 题解 docstring 里如实记录了这个限制。

顺带一个真 bug 的教训:reach 上界截断优化最初会让 dp[reach+1..w] 残留旧值 (应继承 dp[reach]),导致答案偏小。官方样例是过的,靠随机对拍才暴露。

⚠️ BISHI103 必须用 PyPy3 提交。 缩点后是 \(10^4\) 件物品 \(\times\) \(10^4\) 容量的 0/1 背包,即使已经上了「同价剪枝 + 去重 + 二进制拆分 + reach 动态增长」四条优化, 极端数据下仍是 \(10^8\) 级的元素操作,CPython 实测 4.3 秒(时限 2 秒)。 内层已经是最快写法——实测 map(max, ...) 反而比列表推导式慢一倍, 因为 max() 每个元素都要走一次 Python 函数调用。

顺带记一条实测结论:牛客判题机没有装 numpy (提交 import numpy 直接 ModuleNotFoundError), 所以「上 numpy 向量化」这条路在牛客上是走不通的。

提交语言登记在 solutions/_lang.json

题解:solutions/BISHI103.py(已通过牛客判题机验证,PyPy3)

BISHI142:单向依赖 ⇒ 树上分组背包

恰好 \(M\) 门课,选一门必须选它的全部先修课,求最大学分。\(N, M \le 300\)

每门课至多一个直接先修课 ⇒ 先修关系构成森林。加一个编号 0 的虚拟根(学分 0) 把森林接成一棵树,问题变成「在树上选 \(M+1\) 个点(含虚拟根), 且选中点集对父亲封闭,求最大权和」。

\[f[u][j] = \text{在 } u \text{ 的子树里选 } j \text{ 个点、且 } u \text{ 必被选中时的最大学分}\]

初值 \(f[u][1] = s_u\),然后逐个合并孩子:

\[f[u][j] = \max_{0 \le t < j}\big(f[u][j-t] + f[c][t]\big)\]

合并的本质就是分组背包:每个孩子是一组,组内选项是「从这个孩子的子树里拿 \(t\) 个点」。 所以合并时要「先复制一份旧的 \(f[u]\) 再倒着更新」。

复杂度:经典结论是「每对点只在它们的 LCA 处被合并一次」,总合并量 \(O(N^2)\)\(N = 300\) 时不到 \(9\times10^4\)

: 1. 目标是 \(f[0][M+1]\)——虚拟根占一个名额; 2. \(t\) 的上界要同时被「孩子子树大小」和 \(M+1\) 截断,否则退化成 \(O(NM^2)\); 3. 先修链可能深到 300,用迭代式后序遍历(虽然 300 层递归安全,但迭代是通用姿势)。

题解:solutions/BISHI142.py(已通过官方样例验证)


101.10 「至少装满」型

BISHI141 是一个很好的变式:容量维不是「不超过」而是「至少」。

选若干煤炭烧完 \(m\) 单位矿石,至多对一枚施魔法(耗时减半),求最短总时间。 保证 \(n \cdot m \le 10^6\)

把容量维当成「已融化的矿石量」,超过 \(m\) 的部分一律并到 \(m\) 这一格。 实现方式就是把 \(j - v\) 换成 \(\max(0,\ j - v)\)——剩余需求不会变成负数, 多融化的部分不额外记账。

再加一维 0/1 表示魔法是否用过:

\[ \begin{aligned} f_0[j] &\leftarrow \min\big(f_0[j],\ f_0[\max(0, j-x)] + y\big) \\ f_1[j] &\leftarrow \min\big(f_1[j],\ f_1[\max(0,j-x)] + y,\ f_0[\max(0,j-2x)] + \lfloor y/2 \rfloor\big) \end{aligned} \]

Python 写法:把「平移 + 加常数 + 取 min」写成一次批处理:

# [片段]
def shift(d, cap, cost, m):
    """返回数组 g,g[j] = d[max(0, j-cap)] + cost。"""
    # 前 cap 项的 j-cap 都是负数,被 max(0, ...) 夹到 0,所以统一取 d[0]
    # 其余部分就是把 d 整体右移 cap 位再加上 cost,切片长度正好补满到 m
    return [d[0] + cost] * cap + [x + cost for x in d[:m + 1 - cap]]

\(f_1\) 的三个来源必须先全部用旧值算好再一起赋值, 否则会出现「一次操作里用了两次魔法」。

题解:solutions/BISHI141.py(已通过官方样例验证)


101.11 方案数与具体方案

求方案数

\(\max\) 换成 \(+\),初值 \(f[0] = 1\)

# [片段]
f = [0] * (V + 1)
f[0] = 1                              # 「什么都不装」算一种方案,是计数的递推起点
for v, w in items:                    # 01 背包:倒序 / 整段加
    tail = V + 1 - v
    # zip 的两个切片都取自旧的 f:a 是「不选这件」,b 是「选这件」,两类不重不漏
    f[v:] = [(a + b) % MOD for a, b in zip(f[v:], f[:tail])]

注意语义:这里数的是「体积恰好为 \(c\) 的方案数」。 若要数「最优价值的方案数」,需要同时维护 \(f[c]\)(最优值)和 \(g[c]\)(达到最优值的方案数), 转移时比较大小:更优则覆盖 \(g\),相等则累加 \(g\)

输出具体方案

一维滚动数组丢失了「第几件物品」的信息,无法回溯。两种办法:

办法 空间 做法
保留二维 \(f[i][c]\) \(O(nV)\) \(f[n][V]\) 倒推:若 \(f[i][c] = f[i-1][c]\) 则第 \(i\) 件没选,否则选了
记录选择标记 \(O(nV)\) 转移时记下 choose[i][c] = True/False

字典序最小的方案:把物品倒序编号做 DP,回溯时从第 1 件开始贪心地「能选就选」。


101.12 不是背包的「背包题」

BISHI44「灵异背包?」 是一个提醒:题目名带「背包」不代表要写背包。

选出若干数,和为偶数且最大。\(n \le 10^5\)\(a_i \le 2\times10^4\)

\(O(n \cdot S)\) 的背包 DP 是 \(2\times10^9\),必挂。正解是奇偶性 + 贪心

  • 先全选,总和 \(S\)(显然是上界);
  • \(S\) 为偶数 ⇒ 直接输出 \(S\)
  • \(S\) 为奇数 ⇒ 奇数元素的个数必为奇数,必须扔掉奇数个「奇数」。扔得越少越好, 所以只扔一个,且扔最小的那个奇数,答案 \(= S - \min_{\text{odd}}\)

扔偶数元素改变不了奇偶性,只会让和变小,不考虑。\(O(n)\) 一遍扫完。

识别信号:看到「和为偶数」「异或为 0」「模 \(k\)\(r\)」这类条件, 先想想能不能用奇偶性 / 同余的结构直接构造,而不是把它当成背包的一维。

题解:solutions/BISHI44.py(已通过官方样例验证)


101.13 本章速查

形态 容量循环 关键点
01 背包 倒序 用「上一轮」的值
完全背包 正序 用「本轮」的值;Python 改二进制倍增
多重背包 倒序 二进制拆分;先截断 \(s \leftarrow \min(s, m/v)\);特判 \(v=0\)
混合背包 倒序 统一转成 01 物品
二维费用 两维都倒序 内层整行批处理
分组背包 组在外、物品在内 候选一律取自旧 f
双向依赖 倒序 并查集缩点 ⇒ 裸 01 背包
单向依赖 树上分组背包,\(t\) 双重截断
至少装满 倒序 \(j-v\) 换成 \(\max(0, j-v)\)
初值 含义
全 0 不要求装满
f[0]=0,其余 \(-10^{18}\) 恰好装满(不要用 -1)
f[0]=1,其余 0 方案计数
Python 要点 写法
01 背包整段取 max f[v:] = list(map(max, f[v:], [x+w for x in f[:V+1-v]]))
完全背包 二进制倍增,每轮做一次上面的整段取 max
为什么能这样写 候选数组在赋值前用旧 f 算好,01 语义自动成立
提速 比 Python 层循环快 5–8 倍
单调队列优化多重背包 \(O(nm)\),极限数据实测比二进制拆分快 5.4 倍;复杂度优先于常数
语言劣势题 BISHI103 / BISHI138 CPython 必超时,登记为 PyPy3 提交;BISHI137 随机数据偏险