Module 24 · Dynamic Programming

Knapsack-Style DP

Concept~5 min

The shape: items, a capacity, a choice per item

Knapsack DP applies whenever you're selecting from a set of items, each with some cost, under a total capacity constraint, optimizing some value. The canonical state is dp[i][capacity] = the best achievable value using only the first i items, within capacity budget. This lesson names the two variants and their key implementation difference; Coin Change (unbounded) and Partition Equal Subset Sum (0/1) get full problem treatment later in this module.

0/1 knapsack: each item used at most once

The defining constraint: item i is either included or excluded, never reused. The recurrence considers item i explicitly at each step: either skip it (dp[i-1][capacity], unchanged) or take it (add its value to the best achievable with the REMAINING capacity, using only items BEFORE idp[i-1][capacity - weight[i]] + value[i]):

text
dp[i][capacity] = max(dp[i-1][capacity], dp[i-1][capacity - weight[i]] + value[i])

The critical detail: both branches reference dp[i-1][...] — the PREVIOUS item row, never the current one. This is what enforces "used at most once": by the time item i is being decided, its own contribution can only be added on top of a state that has not yet included item i at all.

0/1 knapsack: rows = items, cols = capacity00000003330034400346

Partition Equal Subset Sum (full treatment later) is 0/1 knapsack in disguise: "can a subset sum to exactly target" is "is dp[n][target] achievable," where every number is an item with weight equal to its own value, and capacity is the target sum.

Unbounded knapsack: items reusable without limit

The defining constraint flips: the same item can be used any number of times. The recurrence differs in exactly one place — the "take it" branch references the CURRENT row, not the previous one, since reusing item i again is legal:

text
dp[i][capacity] = max(dp[i-1][capacity], dp[i][capacity - weight[i]] + value[i])

That single index change (dp[i][...] instead of dp[i-1][...] in the "take it" branch) is the entire difference between 0/1 and unbounded knapsack — worth staring at until it's unmistakable, since it's exactly the kind of off-by-one-feeling detail that produces a silently wrong answer rather than a crash.

Coin Change (full treatment later) is unbounded knapsack: "fewest coins to make an amount" reuses each coin denomination without limit, which is why its recurrence allows revisiting the same coin denomination arbitrarily many times while building up the target amount.

The space-optimization subtlety this introduces

Both knapsack variants can be space-optimized to a 1D rolling array (the previous lesson's technique), but 0/1 knapsack has an extra requirement: the capacity dimension must be iterated in REVERSE (decreasing) order when collapsing to 1D, specifically to preserve the "each item at most once" property — iterating forward would let a single item's contribution be read and reused within the same row's update, silently turning 0/1 behavior into unbounded behavior. This subtlety is demonstrated concretely in the Partition Equal Subset Sum problem lesson, where getting the iteration direction wrong is a realistic, easy-to-make bug.

Check yourself

1 question

01

The 0/1 knapsack recurrence's 'take it' branch reads dp[i-1][capacity - weight[i]], while unbounded knapsack's reads dp[i][capacity - weight[i]] — differing only in whether the row index is i-1 or i. Why does this single difference correctly control whether an item can be reused?