Knapsack DP
State is (items considered, capacity used); choose items to maximize value or count/decide subsets hitting a target sum.
Overview
Knapsack DP covers every problem where you select items under a numeric budget: weights against a capacity, coins against an amount, numbers against a target sum. The state is dp[i][w] = best value (or number of ways, or feasibility) using the first i items with capacity w. The two variants differ in one loop direction: 0/1 (each item at most once, 0/1 Knapsack) iterates capacity downward when rolled to one row; unbounded (unlimited copies, Unbounded Knapsack, Coin Change) iterates upward so an item can be reused.
Complexity is O(n · W) time and O(W) space — pseudo-polynomial, because W is a value, not an input length. Typical constraints: n ≤ 100–1000, W ≤ 10^4–10^5, product ≤ ~10^8. If W is 10^9 the intended solution is not this DP.
Members: 0/1 knapsack, subset sum, partition equal subset sum, target sum (+/- assignment), coin change (min coins and number of ways), unbounded knapsack, bounded knapsack (each item up to c_i times — binary-split items into powers of two), and multi-dimensional knapsack (two capacities → dp[w1][w2]).
Intuition
A mental model before the formal terms.
You have a shelf with slots labelled 0..W. Slot w records "the best I can do if my bag can hold exactly w". Go through items one by one; for each item, look at every slot and ask "would adding this item to a bag of capacity w - weight beat what I already have in slot w?".
Why the loop direction matters: if you scan slots from large to small, slot w - weight still holds the value before this item was considered, so the item is used at most once. If you scan from small to large, slot w - weight may already include this item, so you can stack copies — that is precisely unbounded knapsack.
How it works
- State:
dp[i][w]— best value from items0..i-1with capacityw. For counting: number of ways; for feasibility: boolean. - Transition (0/1):
dp[i][w] = max(dp[i-1][w], dp[i-1][w - wt[i-1]] + val[i-1])whenwt[i-1] ≤ w. (Unbounded): replace the second term withdp[i][w - wt[i-1]] + val[i-1](same row, item reusable). - Base cases:
dp[0][w] = 0for max value;dp[0][0] = 1,dp[0][w>0] = 0for counting;dp[0][0] = truefor feasibility;dp[0][w>0] = INFfor min-count. - Order: items outer, capacity inner. Optimization: one row; inner loop
wfromWdown towt(0/1) or fromwtup toW(unbounded). - Counting subtlety: with items outer and capacity inner you count combinations (order of coins irrelevant). With capacity outer and items inner you count permutations (ordered sequences). Choose deliberately.
Why it works
For the last item there are only two futures — it is in the optimal bag or not. In either case the remaining choice is an optimal packing of the previous items into the remaining capacity, so enumerating both and taking the best is exact.
The rolled single row is correct because the downward scan guarantees every read of dp[w - wt] sees the previous item's row; the upward scan guarantees it sees the current item's row, which is the unbounded recurrence.
Recognition
How to tell a problem wants this.
- Choose a subset of items subject to a total weight/cost limit, maximizing value or hitting an exact total.
- "Can the array be partitioned into two equal-sum halves?", "how many ways to make amount
Awith these coins?", "fewest coins". - Constraint on the sum/capacity is moderate (≤ 10^5) while the items are few — the product hints at
O(nW). - The word "unlimited" or "as many times as you like" → unbounded; "each once" → 0/1.
Interactive visualization
Play, step, change the input. ← → and space work too.
Showing the closely related 0/1 Knapsack visualization.
| 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
1# 0/1 knapsack, one row2dp = [0] * (W + 1)3for (wt, val) in items:4 for w from W down to wt: # downward: item used at most once5 dp[w] = max(dp[w], dp[w - wt] + val)6return dp[W]7# unbounded: for w from wt up to W (upward: item reusable)Implementations
1# Representative problem: 0/1 knapsack (max value under capacity W), one-row table2def knapsack01(weights: list[int], values: list[int], W: int) -> int:31 · One-row table over capacities4 dp = [0] * (W + 1) # dp[w] = best value using items seen so far, capacity w52 · Consider items one at a time6 for wt, val in zip(weights, values):73 · Capacity loop DOWNWARD so each item is used at most once8 for w in range(W, wt - 1, -1):94 · Transition: skip item i, or take it10 dp[w] = max(dp[w], dp[w - wt] + val)115 · Read the answer12 return dp[W]13 14 15if __name__ == "__main__":16 print(knapsack01([1, 3, 4, 5], [1, 4, 5, 7], 7)) # 9 (items of weight 3 and 4)- Representative example of knapsack DP: one list over capacities;
zip(weights, values)pairs each item's weight and value. range(W, wt - 1, -1)walks capacities downward, sodp[w - wt]still reflects the state *before* this item — each item is used at most once.- The transition is
max(skip, take)with no conditionals. dp[W]is the best achievable value (9 in the example).
Pure Python manages roughly 10^6–10^7 inner-loop steps per second; large n·W wants PyPy, numpy tricks, or a rethink.
zipover parallel lists is cleaner than index juggling and stops at the shorter list — validate lengths first if they might differ.- For subset-sum feasibility use a
setof reachable sums or anintas a bitset (bits |= bits << wt) — Python's big ints make the bitset trick one line. - The upward-
rangeversion is unbounded knapsack; keep the direction visible in code review.
- Writing
range(W, wt, -1)and never updatingdp[wt]itself. - Iterating capacities upward in the 0/1 variant.
- Rebuilding
dpinside the item loop, which silently resets progress.
- Loop direction carries the semantics in every language: downward = 0/1, upward = unbounded. The bug compiles and runs everywhere.
- Bitset acceleration for subset-sum: C++
std::bitset, Python arbitrary-precision int shifts; JS/TS need a hand-rolled BigInt or typed-array bitset. - Value overflow: C++ picks
intvslong longup front; JS/TS silently lose precision past 2^53; Python is exact.
Complexity
Pseudo-polynomial: W is a magnitude, not a length. Bounded knapsack with binary splitting is O(W · Σ log c_i).
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Selecting items under a numeric budget with moderate budget size.
- Exact-sum feasibility or counting over a set of numbers (partition, target sum).
- Coin systems that are not canonical, where greedy fails (coins
{1, 3, 4}, amount 6).
- Capacity is huge (10^9+) —
O(nW)is infeasible; consider meet-in-the-middle forn ≤ 40, or greedy if the problem is Fractional Knapsack. - Items are divisible — Fractional Knapsack is greedy by value density.
- The coin system is canonical (1, 5, 10, 25) and only the minimum count is asked — greedy works, though DP is still safe.
Alternatives
Common mistakes
- Iterating capacity upward in 0/1 knapsack — silently turns it into unbounded.
- Counting permutations when combinations were asked (or vice versa) by swapping the loop nesting.
- Initializing min-coins
dpwith0instead ofINF, so every amount looks free. - Sizing the table
Winstead ofW + 1. - Forgetting the "item does not fit" branch when
wt > w(should just carrydp[i-1][w]).
Interview patterns
- 0/1 Knapsack, Subset Sum, Partition Equal Subset Sum, Target Sum (transform to subset count).
- Coin Change (min coins) and Coin Change II (number of combinations).
- Last Stone Weight II (minimize difference = subset sum closest to half).
- Ones and Zeroes (2D capacity), Profitable Schemes (capacity + count).
- Coin ChangeIntermediate