pack-skill — 背包问题九讲 in skill form
What "philosophically a knapsack problem" means
A problem belongs to this family iff it can be cast as:
Given a finite collection of items, each consuming some cost(s) when taken and yielding some value, choose a feasible selection under a capacity to optimize an aggregate of values.
Under that frame, every variant reduces to one master DP: F[i, v] = combine over item-i policies of F[i−1, v − cost(policy)] ⊕ value(policy). The job of this skill is to (a) recognize the variant, (b) write the right state + transition + iteration order, (c) handle the question-form (max value / count / output plan / Kth-best).
Workflow — apply in order
- Recognize the variant. Read
references/01-recognize.md and run the decision tree there. Identify: how many copies per item, single/multi cost, item interactions (exclusive groups, dependencies), and what the question asks.
- Design the state. Read
references/02-state-design.md. Decide: dimensions of F, init values (max-value vs exact-fill), and 1-D space compression direction.
- Pick the canonical procedure for each item type and call it in the right loop order. The procedures are reusable Python functions in
scripts/pack_primitives.py:
zero_one_pack(F, C, W) — item taken 0 or 1 time
complete_pack(F, C, W) — item taken unlimited times
multiple_pack(F, C, W, M) — item taken 0..M times (binary-splitting)
- Handle the question form. Output plan, count schemes, count optimal schemes, Kth-best — see
references/11-variations.md and the corresponding scripts.
- Verify correctness on the simple cases first — for any non-trivial setup, brute-force enumerate up to N≤20 and diff against the DP.
scripts/pack_primitives.py includes a brute_max helper for this.
Reference map
Read the file matching the variant you identified in step 1. Each is self-contained.
| Variant |
Reference file |
Script |
| Recognize / decision tree |
references/01-recognize.md |
— |
| State design, init, space opt |
references/02-state-design.md |
— |
| 01 pack (0/1) |
references/03-zero-one.md |
scripts/solve_01.py |
| Complete pack (unbounded) |
references/04-complete.md |
scripts/solve_complete.py |
| Multiple pack (bounded M_i) |
references/05-multiple.md |
scripts/solve_multiple.py |
| Mixed (01 + complete + bounded) |
references/06-mixed.md |
scripts/solve_mixed.py |
| 2-D cost (two capacities) |
references/07-2d-cost.md |
scripts/solve_2d.py |
| Grouped (mutually exclusive groups) |
references/08-grouped.md |
scripts/solve_grouped.py |
| Dependency (main / accessory, tree) |
references/09-dependency.md |
scripts/solve_dependency.py |
| Generalized items (泛化物品 abstraction) |
references/10-generalized.md |
— |
| Question variations |
references/11-variations.md |
scripts/count_schemes.py, scripts/count_optimal.py, scripts/output_solution.py, scripts/kth_optimal.py |
The five invariants (read once, never forget)
These come straight from chapters 1–9. They are the rules every variant obeys.
- Master transition.
F[i,v] = max{ F[i-1, v - k·Cᵢ] + k·Wᵢ } over the legal copy-counts k for item i. Every variant differs only by which k are legal.
- 1-D direction encodes copies.
- 0/1 item → iterate
v descending from V to Cᵢ (so each Wᵢ is added at most once).
- Unbounded item → iterate
v ascending from Cᵢ to V (so Wᵢ may cascade).
- Bounded → binary-split into
O(log Mᵢ) 0/1 items.
- Init encodes "exact fill" vs "≤ capacity".
- Must fill exactly:
F[0]=0, F[1..V] = -∞.
- Just ≤ V:
F[0..V] = 0.
- Grouped/dependency = mutually exclusive policies per unit. Loop order is
for group → for v descending → for item-in-group, never permute these three.
- 泛化物品 (generalized item) is the universal abstraction. Any item / group / subtree corresponds to a function
h: [0..V] → value. "Sum" of two generalized items is f(v) = max_{0≤k≤v} h(k)+l(v-k), O(V²). Most "hard-looking" knapsacks are just a sum of generalized items.
Common pitfalls
- Wrong iteration direction → silently overcounts (treats 0/1 as unbounded) or undercounts.
- Forgetting the exact-fill init → answers max-value when the problem says "fill exactly".
- Grouped pack with v-loop inside item-loop → lets multiple items of the same group be taken.
- Multiple pack with M·C ≥ V → just call
complete_pack; don't binary-split.
- Dependency pack as plain group → wrong; you must first 0/1-DP the accessory set into a generalized item of length
V − C_main + 1 (see chapter 7).
- Counting schemes → replace
max with sum, and init F[0,0]=1 (or F[0]=1 in 1-D).
- Lex-smallest plan → renumber items as
x ← N+1−x first, then run the standard recovery (chapter 9.2).
Quick start — solving a fresh problem
Q: N items, each with weight wᵢ and value vᵢ, knapsack capacity W, each item used at most once.
Maximize total value.
This is plain 0/1 pack. Run:
python3 scripts/solve_01.py --capacity W --items "w1,v1; w2,v2; ..."
Or import the primitive:
from pack_primitives import zero_one_pack
F = [0] * (W + 1)
for c, w in items:
zero_one_pack(F, c, w)
print(F[W]) # max value with capacity ≤ W
For "fill exactly W": initialize F = [0] + [float('-inf')] * W instead.
When the problem doesn't look like a knapsack but is one
Recognize these disguises (each is treated in references/01-recognize.md):
- Coin change — number of ways: complete pack +
sum instead of max, F[0]=1.
- Subset sum / partition equal subset: 0/1 pack with W=value, ask "is
F[target] reachable".
- Cutting rod: complete pack with
C = length, W = price.
- Target sum / +/- signs: shift to subset-sum form
target = (S + target_diff)/2.
- NOIP 金明的预算方案: dependency pack (main + ≤2 accessories) — see chapter 7.
- NOIP 采药 / 装箱问题: textbook 0/1 pack.
- Multi-dimensional resource scheduling (cost in cash AND time, etc.): 2-D cost pack.
Verification
Whenever you write a new knapsack DP, brute-force check it on small inputs:
from pack_primitives import brute_max_01
assert dp_answer == brute_max_01(items, W)
scripts/pack_primitives.py ships brute_max_01, brute_max_complete, brute_max_multiple for this.
1---2name: pack-skill3description: Solve any problem in the philosophical family of the knapsack problem (背包问题) using the unified state-design framework from 崔添翼《背包问题九讲》. Use whenever a problem reduces to "choose a subset/multiset of items under a capacity-like cost to optimize a value-like objective" — including 0/1 pack, unbounded/complete pack, bounded/multiple pack, mixed pack, 2-D cost pack, grouped pack, dependency pack (主件/附件, tree-shaped), generalized items (泛化物品), subset-sum / can-it-fill-exactly, counting feasible or optimal schemes, recovering the chosen subset, lexicographically smallest plan, and Kth-best value. Trigger words — 背包/knapsack, subset sum, "choose items with weight/cost ≤ W to maximize value", "ways to make change", "partition into K groups", "limited copies", "items depend on each other", "fill exactly", coin change counting, NOIP-style 金明的预算方案 / 采药 / 装箱问题.4---56# pack-skill — 背包问题九讲 in skill form78## What "philosophically a knapsack problem" means910A problem belongs to this family iff it can be cast as:1112> Given a finite collection of **items**, each consuming some **cost(s)** when taken and yielding some **value**, choose a feasible selection under a **capacity** to optimize an aggregate of values.1314Under that frame, every variant reduces to one master DP: `F[i, v] = combine over item-i policies of F[i−1, v − cost(policy)] ⊕ value(policy)`. The job of this skill is to (a) recognize the variant, (b) write the right state + transition + iteration order, (c) handle the question-form (max value / count / output plan / Kth-best).1516## Workflow — apply in order17181. **Recognize the variant.** Read `references/01-recognize.md` and run the decision tree there. Identify: how many copies per item, single/multi cost, item interactions (exclusive groups, dependencies), and what the question asks.192. **Design the state.** Read `references/02-state-design.md`. Decide: dimensions of `F`, init values (max-value vs exact-fill), and 1-D space compression direction.203. **Pick the canonical procedure** for each item type and call it in the right loop order. The procedures are reusable Python functions in `scripts/pack_primitives.py`:21 - `zero_one_pack(F, C, W)` — item taken 0 or 1 time22 - `complete_pack(F, C, W)` — item taken unlimited times23 - `multiple_pack(F, C, W, M)` — item taken 0..M times (binary-splitting)244. **Handle the question form.** Output plan, count schemes, count optimal schemes, Kth-best — see `references/11-variations.md` and the corresponding scripts.255. **Verify correctness on the simple cases first** — for any non-trivial setup, brute-force enumerate up to N≤20 and diff against the DP. `scripts/pack_primitives.py` includes a `brute_max` helper for this.2627## Reference map2829Read the file matching the variant you identified in step 1. Each is self-contained.3031| Variant | Reference file | Script |32|---|---|---|33| Recognize / decision tree | `references/01-recognize.md` | — |34| State design, init, space opt | `references/02-state-design.md` | — |35| 01 pack (0/1) | `references/03-zero-one.md` | `scripts/solve_01.py` |36| Complete pack (unbounded) | `references/04-complete.md` | `scripts/solve_complete.py` |37| Multiple pack (bounded M_i) | `references/05-multiple.md` | `scripts/solve_multiple.py` |38| Mixed (01 + complete + bounded) | `references/06-mixed.md` | `scripts/solve_mixed.py` |39| 2-D cost (two capacities) | `references/07-2d-cost.md` | `scripts/solve_2d.py` |40| Grouped (mutually exclusive groups) | `references/08-grouped.md` | `scripts/solve_grouped.py` |41| Dependency (main / accessory, tree) | `references/09-dependency.md` | `scripts/solve_dependency.py` |42| Generalized items (泛化物品 abstraction) | `references/10-generalized.md` | — |43| Question variations | `references/11-variations.md` | `scripts/count_schemes.py`, `scripts/count_optimal.py`, `scripts/output_solution.py`, `scripts/kth_optimal.py` |4445## The five invariants (read once, never forget)4647These come straight from chapters 1–9. They are the rules every variant obeys.48491. **Master transition.** `F[i,v] = max{ F[i-1, v - k·Cᵢ] + k·Wᵢ }` over the legal copy-counts `k` for item `i`. Every variant differs only by which `k` are legal.502. **1-D direction encodes copies.**51 - 0/1 item → iterate `v` **descending** from `V` to `Cᵢ` (so each Wᵢ is added at most once).52 - Unbounded item → iterate `v` **ascending** from `Cᵢ` to `V` (so Wᵢ may cascade).53 - Bounded → binary-split into `O(log Mᵢ)` 0/1 items.543. **Init encodes "exact fill" vs "≤ capacity".**55 - Must fill exactly: `F[0]=0, F[1..V] = -∞`.56 - Just ≤ V: `F[0..V] = 0`.574. **Grouped/dependency = mutually exclusive policies per unit.** Loop order is `for group → for v descending → for item-in-group`, never permute these three.585. **泛化物品 (generalized item) is the universal abstraction.** Any item / group / subtree corresponds to a function `h: [0..V] → value`. "Sum" of two generalized items is `f(v) = max_{0≤k≤v} h(k)+l(v-k)`, `O(V²)`. Most "hard-looking" knapsacks are just a sum of generalized items.5960## Common pitfalls6162- **Wrong iteration direction** → silently overcounts (treats 0/1 as unbounded) or undercounts.63- **Forgetting the exact-fill init** → answers max-value when the problem says "fill exactly".64- **Grouped pack with v-loop inside item-loop** → lets multiple items of the same group be taken.65- **Multiple pack with M·C ≥ V** → just call `complete_pack`; don't binary-split.66- **Dependency pack as plain group** → wrong; you must first 0/1-DP the accessory set into a generalized item of length `V − C_main + 1` (see chapter 7).67- **Counting schemes** → replace `max` with `sum`, and init `F[0,0]=1` (or `F[0]=1` in 1-D).68- **Lex-smallest plan** → renumber items as `x ← N+1−x` first, then run the standard recovery (chapter 9.2).6970## Quick start — solving a fresh problem7172```text73Q: N items, each with weight wᵢ and value vᵢ, knapsack capacity W, each item used at most once.74 Maximize total value.75```7677This is plain 0/1 pack. Run:7879```bash80python3 scripts/solve_01.py --capacity W --items "w1,v1; w2,v2; ..."81```8283Or import the primitive:8485```python86from pack_primitives import zero_one_pack87F = [0] * (W + 1)88for c, w in items:89 zero_one_pack(F, c, w)90print(F[W]) # max value with capacity ≤ W91```9293For "fill exactly W": initialize `F = [0] + [float('-inf')] * W` instead.9495## When the problem doesn't look like a knapsack but is one9697Recognize these disguises (each is treated in `references/01-recognize.md`):9899- **Coin change — number of ways**: complete pack + `sum` instead of `max`, `F[0]=1`.100- **Subset sum / partition equal subset**: 0/1 pack with W=value, ask "is `F[target]` reachable".101- **Cutting rod**: complete pack with `C = length, W = price`.102- **Target sum / +/- signs**: shift to subset-sum form `target = (S + target_diff)/2`.103- **NOIP 金明的预算方案**: dependency pack (main + ≤2 accessories) — see chapter 7.104- **NOIP 采药 / 装箱问题**: textbook 0/1 pack.105- **Multi-dimensional resource scheduling** (cost in cash AND time, etc.): 2-D cost pack.106107## Verification108109Whenever you write a new knapsack DP, brute-force check it on small inputs:110111```python112from pack_primitives import brute_max_01113assert dp_answer == brute_max_01(items, W)114```115116`scripts/pack_primitives.py` ships `brute_max_01`, `brute_max_complete`, `brute_max_multiple` for this.