Knapsack Patterns
Quick reference
| 0/1 Knapsack β full 2D table | O(n Γ capacity) |
| 0/1 Knapsack β 1D, backward scan | O(n Γ capacity) |
| Unbounded Knapsack (Coin Change, etc.) | O(n Γ capacity) |
You pack a hiking backpack. Every item that you could bring has a weight and a "how much you would miss it" value. The bag holds only so much weight before your shoulders give out. You want the combination of items that packs the most value in without going over the limit. This is the knapsack problem. It is the same "remember what you have already solved" idea as every other DP topic so far, but with a budget (capacity) as the second axis instead of a grid coordinate.
The core insight: dp[i][capacity] = the best value you can reach with only the first i items and this much capacity left to spend. For each item, there are exactly two choices. Leave it out, in which case dp[i][capacity] is whatever it was without this item (dp[i-1][capacity]). Or take it, in which case you gain its value but spend its weight (its value + dp[i-1][capacity - its weight]). dp[i][capacity] is whichever of those two is bigger. That is the whole algorithm. Everything below is to apply it carefully.
0/1 vs. unbounded β same table, opposite scan direction
# dp[i][capacity] = best value using the first i items, this much capacity left
function knapsack01(items, capacity):
dp = 2D array, (n + 1) x (capacity + 1), all zeros
for i from 1 to n:
for c from 0 to capacity:
dp[i][c] = dp[i - 1][c] // leave item i-1 out
if items[i - 1].weight <= c:
dp[i][c] = max(dp[i][c],
items[i - 1].value + dp[i - 1][c - items[i - 1].weight]) // take it
return dp[n][capacity]0/1 Knapsack β full 2D table
function knapsack01(weights, values, capacity):
n = len(weights)
dp = 2D array, (n + 1) x (capacity + 1), all zeros
for i from 1 to n:
for c from 0 to capacity:
dp[i][c] = dp[i - 1][c] // do not take item i-1
if weights[i - 1] <= c:
dp[i][c] = max(dp[i][c], values[i - 1] + dp[i - 1][c - weights[i - 1]]) // take it
return dp[n][capacity]0/1 Knapsack, space-optimized β 1D, capacity iterated BACKWARDS
function knapsack01(weights, values, capacity):
dp = array of size capacity + 1, all zeros
for i from 0 to len(weights) - 1:
for c from capacity down to weights[i]: // β backwards, not forwards
dp[c] = max(dp[c], values[i] + dp[c - weights[i]])
return dp[capacity]Unbounded Knapsack (e.g. Coin Change) β capacity iterated FORWARDS
function coinChange(coins, amount):
dp = array of size amount + 1
dp[0] = 0
for c from 1 to amount:
dp[c] = infinity
for c from 1 to amount: // forward: reuse is intentional
for coin in coins:
if coin <= c:
dp[c] = min(dp[c], 1 + dp[c - coin])
return dp[amount] if dp[amount] != infinity else -1The 2D table of 0/1 knapsack costs O(n Γ capacity) time and space, one entry per (item count, capacity) pair, each filled in O(1). The 1D rolling trick above drops space to O(capacity): only the previous item's row is ever read. Unbounded knapsack has the identical O(n Γ capacity) time and space shape. The forward-versus-backward scan changes correctness, not the asymptotic cost.
| Operation | Time | Space | Why |
|---|---|---|---|
| 0/1 Knapsack β full 2D table | O(n Γ capacity) | O(n Γ capacity) | One dp[i][c] is computed per (item, capacity) pair, with O(1) work each. |
| 0/1 Knapsack β 1D, backward scan | O(n Γ capacity) | O(capacity) | The same number of cells is filled. Only one row of the table needs to exist at a time, because each item's row reads only the row before it. |
| Unbounded Knapsack (Coin Change, etc.) | O(n Γ capacity) | O(capacity) | This is the same shape as the 1D version of 0/1. The forward scan is what allows reuse, not a change in how many cells are touched. |
Iterating capacity forward in the 0/1 space-optimized version
This is the most important knapsack bug, worth real attention. In the 1D version, dp[c - weight] on the right-hand side must still be the "last item's" value. A forward scan updates a smaller capacity first. A larger capacity's update then reads that already-updated (this item's) value. This quietly lets the same item be packed two times in one call, which turns 0/1 knapsack into unbounded knapsack by accident. The fix is always: 0/1 scans capacity backward.
Confusing 0/1 vs. unbounded iteration direction for the problem actually being solved
The two scan directions are not interchangeable defaults. Which one is correct depends completely on whether the problem allows reuse of an item. Partition Equal Subset Sum (each number used one time) needs the backward scan. Coin Change (each coin denomination reusable) needs the forward scan. To copy one pattern onto the other problem produces code that runs fine and returns a plausible-looking wrong answer.
Getting dp[0][*] / dp[*][0] base cases backwards
dp[0][c] = 0 for every c (no items to choose from means zero value, however much room there is). dp[i][0] = 0 for every i (no room means nothing fits, however many options there are). If you swap which index represents "items used" and which represents "capacity", or seed only one of the two zero-rows, you produce a table that is correct in one direction and quietly wrong in the other.
Trusting greedy instead of knapsack DP
The coin-change counterexample from Basic Greedy applies here directly. 0/1 knapsack is exactly a case where the greedy "take the best ratio first" instinct is provably not optimal. Consider capacity 10 with items (weight 6, value 30), (weight 5, value 24), and (weight 5, value 24). Greedy takes the weight-6 item first (the best ratio, value 5.0) and then has room for nothing else, for a total of 30. But to skip it for the two weight-5 items gives 48. Greedy locks in a choice that it can never undo. Knapsack DP considers both options at every step and cannot be fooled this way.
Sign in to mark problems done β progress syncs across devices.