0/1 Knapsack
Choose a subset of items, each used at most once, maximizing total value without exceeding a weight capacity.
Overview
Given n items with weights w[i] and values v[i] and a knapsack of capacity W, pick a subset with total weight ≤ W and maximum total value. Each item is either taken or left — hence "0/1". The problem is NP-hard in general, but when W is a moderate integer the DP over (items, capacity) runs in O(n·W), which is pseudo-polynomial.
It is the parent of a whole family — subset sum, partition equal subset sum, target sum, and the unbounded and bounded variants (see Knapsack DP). Learn this one thoroughly; the others are edits to the transition or the loop direction.
Intuition
A mental model before the formal terms.
Take three items: weights {1, 3, 4}, values {15, 20, 30}, capacity W = 4. Greedy by value/weight ratio takes item 1 (ratio 15), then item 2 (ratio 6.67) — total weight 4, value 35. But {item 3} alone gives weight 4, value 30, and {item 1, item 2} gives 35; the true optimum is 35 here, but change item 3 to value 40 and greedy still returns 35 while the optimum is 40. The point: whether to take an item depends on the *whole* remaining budget, not on a local ratio.
The DP asks one question per item: "with capacity c, is the best I can do with items 1..i better by skipping item i, or by taking it and asking the same question for items 1..i-1 with capacity c - w[i]?" Fill a table of answers to all those questions and the top-right cell is the result.
How it works
- State:
dp[i][c]= maximum value using only the firstiitems with total weight ≤c. Table size(n+1) × (W+1). - Transition:
dp[i][c] = dp[i-1][c](skip itemi); ifw[i] ≤ calso considerdp[i-1][c - w[i]] + v[i](take it) and keep the max. Both branches read rowi-1only. - Base case:
dp[0][c] = 0for allc(no items, no value). If the question is "exactly weightc" instead of "at most", use-∞fordp[0][c>0]. - Iteration order:
ifrom 1 ton(outer),cfrom 0 toW(inner, any direction in the 2D version). - Answer location:
dp[n][W]. To reconstruct the chosen items, walk back from(n, W): ifdp[i][c] != dp[i-1][c]itemiwas taken andc -= w[i]. - Space optimization: row
idepends only on rowi-1, so keep one arraydp[c]and iteratecfrom `W` down to `w[i]`. The descending order guaranteesdp[c - w[i]]still holds the previous row's value (iteminot yet used) whendp[c]reads it. Ascending order would allow itemito be counted twice — that is exactly Unbounded Knapsack.
Why it works
Optimal substructure: consider any optimal solution for items 1..i and capacity c. Either it excludes item i, in which case it is an optimal solution for (i-1, c) — if a better one existed we could swap it in. Or it includes item i, in which case the rest is an optimal solution for (i-1, c - w[i]) by the same exchange argument. The transition considers both cases, so it computes the true optimum.
Overlapping subproblems: the recursive tree has 2^n leaves but only (n+1)(W+1) distinct (i, c) pairs. Tabulation touches each once with O(1) work.
The 1D reverse-loop trick is correct because writing dp[c] for large c first never disturbs dp[c'] for c' < c, which are the only cells later iterations read.
Recognition
How to tell a problem wants this.
- "Choose a subset" + "each item at most once" + "capacity / budget / limit" + "maximize value" or "is a total achievable".
- Constraints like
n ≤ 100,W ≤ 10^4orsum(nums) ≤ 2·10^4— small enough forn·Wbut far too large for2^n. - Subset sum, partition into two equal halves, target sum with ± signs — all disguised 0/1 knapsacks with a boolean or counting table.
Interactive visualization
Play, step, change the input. ← → and space work too.
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
|---|---|---|---|---|---|---|---|---|
| ∅ | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| w1 v1 | · | · | · | · | · | · | · | · |
| w3 v4 | · | · | · | · | · | · | · | · |
| w4 v5 | · | · | · | · | · | · | · | · |
| w5 v7 | · | · | · | · | · | · | · | · |
1dp[0][*] = 02for i in 1 .. n:3 for c in 0 .. W:4 dp[i][c] = dp[i-1][c] // skip item i5 if w[i] <= c:6 dp[i][c] = max(dp[i][c], dp[i-1][c-w[i]] + v[i]) // take7traceback from dp[n][W]Pseudocode
1dp = array[W + 1] filled with 02for i in 0..n-1:3 for c from W down to w[i]:4 dp[c] = max(dp[c], dp[c - w[i]] + v[i])5return dp[W]Implementations
1# dp[i][c] = best value using the first i items with capacity c2def knapsack_01(weights: list[int], values: list[int], W: int) -> int:31 · State table4 n = len(weights)5 dp = [[0] * (W + 1) for _ in range(n + 1)]62 · Base cases7 # row 0 (no items) is all zeros — already set by the comprehension83 · Transition9 for i in range(1, n + 1):10 w, v = weights[i - 1], values[i - 1]11 for c in range(W + 1):12 dp[i][c] = dp[i - 1][c] # skip item i13 if w <= c:14 dp[i][c] = max(dp[i][c], dp[i - 1][c - w] + v) # take it154 · Answer16 return dp[n][W]17 18 195 · Reconstruction20def knapsack_01_items(weights: list[int], values: list[int], W: int) -> list[int]:21 n = len(weights)22 dp = [[0] * (W + 1) for _ in range(n + 1)]23 for i in range(1, n + 1):24 w, v = weights[i - 1], values[i - 1]25 for c in range(W + 1):26 dp[i][c] = dp[i - 1][c]27 if w <= c:28 dp[i][c] = max(dp[i][c], dp[i - 1][c - w] + v)29 chosen: list[int] = []30 c = W31 for i in range(n, 0, -1):32 if dp[i][c] != dp[i - 1][c]: # value changed => item i was taken33 chosen.append(i - 1)34 c -= weights[i - 1]35 return chosen[::-1][[0] * (W + 1) for _ in range(n + 1)]creates a fresh inner list per row —[[0] * (W + 1)] * (n + 1)would alias one row n+1 times.- Row 0 is all zeros, the base case.
w, v = weights[i - 1], values[i - 1]converts 1-based table row to 0-based item index once per row.- The transition first copies the skip value, then applies
maxwith the take branch. - Reconstruction walks rows backward;
chosen[::-1]restores ascending order.
Python lists of ints are pointer arrays; for W in the 10^5 range the 2D table can be hundreds of MB — use the 1D form or array("i").
- 1D rolling form:
for c in range(W, w - 1, -1): dp[c] = max(dp[c], dp[c - w] + v)— thew - 1stop is inclusive ofw. zip(weights, values)iterates item pairs without index bookkeeping.lru_cacheon a nested function keyed by(i, cap)is the top-down form; recursion depth is n, which is fine for n ≤ ~900.
- Aliased rows from
[[0] * (W + 1)] * (n + 1)— updates todp[1]show up in every row. - Writing
range(W, w, -1)and never processing capacity exactlyw. - Confusing the 1-based table index
iwith the 0-based item indexi - 1.
- 2D table construction: C++ nested
std::vectorconstructor copies the inner vector per row (safe); JS/TSfill(row)and Python[row] * nboth alias a single row — useArray.fromwith a factory / a list comprehension. - Overflow: C++
intwraps when the summed values exceed 2^31 (uselong long); JS/TS stay exact below 2^53; Python is exact. - Memo key for (i, cap): C++ 2D vector, JS/TS numeric composite key in a
Map, Python tuple vialru_cache. - Descending range: C++
for (c = W; c >= w; c--), JS/TS the same, Pythonrange(W, w - 1, -1)(stop is exclusive).
Complexity
Pseudo-polynomial: depends on the numeric value of W, not its bit length. 2D table (needed for reconstruction) uses O(n·W) space.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Subset selection under one additive constraint with each item usable at most once, where the capacity is a moderate integer.
- Feasibility variants (subset sum, partition) with a boolean table:
dp[c] |= dp[c - w]. - Counting variants ("how many subsets sum to target"):
dp[c] += dp[c - w], same downward loop.
- Capacity is huge (
10^9) or fractional — the table does not fit; use meet-in-the-middle for smalln, branch and bound, or approximation. - Items are divisible — Fractional Knapsack is greedy and optimal in
O(n log n). - Items can be reused without limit — that is Unbounded Knapsack, which uses the ascending loop.
Alternatives
Common mistakes
- Iterating capacity upward in the 1D version — silently turns the problem into unbounded knapsack.
- Confusing "at most W" with "exactly W" and initializing the base row with 0 in both cases.
- Reconstructing the chosen items from the 1D array — it has no history; keep the 2D table or store parent pointers.
- Sizing the table with
Winstead ofW + 1, dropping capacityWitself.
Interview patterns
- Partition Equal Subset Sum: boolean subset sum to
total / 2. - Target Sum: count subsets with sum
(total + target) / 2. - Last Stone Weight II: minimize
total - 2·Sover achievable subset sumsS ≤ total / 2. - Ones and Zeroes: 0/1 knapsack with two capacities (2D rolling table, both loops downward).
- Coin ChangeIntermediate