LearnAbout

Knapsack Patterns

On this page
Quick reference
0/1 Knapsack β€” full 2D tableO(n Γ— capacity)
0/1 Knapsack β€” 1D, backward scanO(n Γ— capacity)
Unbounded Knapsack (Coin Change, etc.)O(n Γ— capacity)
The Idea

You're packing a hiking backpack. Every item you could bring has a weight and a "how much you'd miss it" value, and the bag only holds 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. That's the knapsack problem, and it's the same "remember what you've already solved" idea as every other DP topic so far β€” just with a budget (capacity) as the second axis instead of a grid coordinate.

The core insight: dp[i][capacity] = the best value achievable using only the first i items, with 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's the entire algorithm; everything below is applying it carefully.

0/1 knapsack means each item can be used at most once β€” you either pack it or you don't. Unbounded knapsack means each item can be used any number of times, like coins when making change. Same recurrence shape, one small but critical difference in how the loop runs β€” Build It below makes that difference concrete.
Build It
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]                    // don't 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]
dp[0][*] = 0 (no items means no value, at any capacity) is the whole base case, and it falls out naturally from initializing the table to zeros β€” no special-casing needed here, unlike Grid DP's first row/column.
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]
Iterating capacity backwards means dp[c - weights[i]] on the right-hand side still refers to LAST item's table (the row above, in 2D terms) β€” it hasn't been overwritten yet for this item. Iterate forwards instead and dp[c - weights[i]] might already reflect item i having been used, letting the same item get counted twice in one pass. This single direction flip is the entire difference between 0/1 and unbounded knapsack.
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 -1
Scanning capacity forward means dp[c - coin] can already include this same coin used earlier in this very pass β€” which is exactly what "unlimited supply of each coin" requires. That forward direction is the one-line difference from 0/1 knapsack's backward scan, and it's not a stylistic choice: get it backwards here and coins can never be reused, which is a different, wrong problem.
Know It

0/1 knapsack's 2D table 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-vs-backward scan changes correctness, not the asymptotic cost.

OperationTimeSpaceWhy
0/1 Knapsack β€” full 2D tableO(n Γ— capacity)O(n Γ— capacity)One dp[i][c] computed per (item, capacity) pair, O(1) work each.
0/1 Knapsack β€” 1D, backward scanO(n Γ— capacity)O(capacity)Same number of cells filled; only one row of the table needs to exist at a time because each item's row only ever reads the row before it.
Unbounded Knapsack (Coin Change, etc.)O(n Γ— capacity)O(capacity)Same shape as 0/1's 1D version β€” the forward scan is what allows reuse, not a change in how many cells get touched.
Break It

Iterating capacity forward in the 0/1 space-optimized version

the 1D dp array is scanned low-to-high instead of high-to-low

This is the single most important knapsack bug, worth real attention: in the 1D version, dp[c - weight] on the right-hand side needs to still be "last item's" value. Scanning forward means a smaller capacity gets updated first, and a larger capacity's update then reads that already-updated (this item's) value β€” silently letting the same item get packed twice in one call, turning 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

an item can be reused (coins, unlimited stock) but the code scans backward, or vice versa

The two scan directions are not interchangeable defaults β€” which one is correct depends entirely on whether the problem allows reusing an item. Partition Equal Subset Sum (each number used once) needs the backward scan; Coin Change (each coin denomination reusable) needs the forward scan. Copy-pasting 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

zero items or zero capacity

dp[0][c] = 0 for every c (no items to choose from means zero value, no matter how much room there is) and dp[i][0] = 0 for every i (no room means nothing fits, no matter how many options there are). Swapping which index represents "items used" and which represents "capacity" β€” or seeding only one of the two zero-rows β€” produces a table that's correct in one direction and silently wrong in the other.

Trusting greedy instead of knapsack DP

picking items by best value-to-weight ratio first, for 0/1 knapsack

Basic Greedy's own coin-change counterexample 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) and (weight 5, value 24) and (weight 5, value 24): greedy grabs the weight-6 item first (best ratio, value 5.0) and then only has room for nothing else β€” total 30. But skipping it for the two weight-5 items gives 48. Greedy locks in a choice it can never undo; knapsack DP considers both options at every step and can't be fooled this way.

Use It
Partition Equal Subset Sum
0/1 knapsack in disguise: can a subset of nums sum to exactly total/2? Capacity is total/2, every number's "value" and "weight" are itself, scan backward.
Medium
Target Sum
Reframe +/- assignments as a subset-sum split (positives sum to P, negatives to N, P - N = target, P + N = total) β€” then it's Partition Equal Subset Sum's shape, counting ways instead of just checking feasibility.
Medium
Coin Change
coinChange() from Build It, verbatim β€” the unbounded knapsack shape, and a direct callback to Basic Greedy's own coin-change counterexample (why greedy fails on denominations like {1, 3, 4}).
Medium
Coin Change II
Same unbounded shape as Coin Change, but count combinations instead of minimizing coins: dp[c] += dp[c - coin], coins as the outer loop so order doesn't create duplicate combinations.
Medium
Last Stone Weight II
Equivalent to Partition Equal Subset Sum: find the subset closest to total/2, and the answer is total - 2 Γ— (best achievable subset sum).
Medium
Ones and Zeroes
0/1 knapsack with two capacities at once β€” dp[zeros][ones], scanning both backward per string, taking each string costs some of the zero-budget and some of the one-budget.
Medium
Perfect Squares
Unbounded knapsack shape: "coins" are 1Β², 2Β², 3Β², ... up to n, minimize how many sum to exactly n.
Medium
Combination Sum IV
Revisit from Backtracking, now solved with unbounded-knapsack DP instead of exhaustive search β€” but order matters this time (different orderings count as different combinations), so capacity is the outer loop and candidates the inner loop, the reverse of Coin Change II.
Medium