Knapsack DP
Decide which items to take under a capacity or exact-sum constraint by building answers for every smaller capacity, in 0/1, unbounded and counting flavours.
Instinct Choose items to hit or stay under a target sum (can it be done, best value, fewest items, how many ways) - index a DP array by the remaining capacity.
Memory hack Once? Walk back. Many? Walk forward. Each item used at most once loops capacity downward; reusable items loop it upward.
At a glance
When to use it
- You get a bag of numbers and a target sum, capacity or amount
- Each item is either taken or not, once or an unlimited number of times
- You must decide feasibility, the best value, the fewest items or the number of ways
- The target is small enough to index an array by it
Key idea
Index the DP by the remaining capacity instead of by subsets, so O(2^n) enumeration becomes O(n x T); loop capacity downward for use-once items and upward for reusable ones.
Typical complexity
O(n x T) time, O(T) space
| # | Common problems | Level | Our solution | Practice |
|---|---|---|---|---|
| 1 | Coin Change | Medium | Read solution | LeetCode ↗ |
| 2 | Partition Equal Subset Sum | Medium | Read solution | LeetCode ↗ |
| 3 | Perfect Squares | Medium | Read solution | LeetCode ↗ |
| 4 | Coin Change II | Medium | Read solution | LeetCode ↗ |
| 5 | Target Sum | Medium | Read solution | LeetCode ↗ |
| 6 | Last Stone Weight II | Medium | Read solution | LeetCode ↗ |
The instinct
Whenever a problem hands you a bag of numbers and a target (a capacity, a sum, an amount), and every item is either used or not, think knapsack. Trying every subset is 2ⁿ, but the target is usually small, so you can ask instead: "for every sum from 0 to target, what is the best I can do using the items seen so far?" The DP dimension is the sum, not the subset.
Then answer two questions: May each item be reused? (0/1 vs unbounded) and What do I store per sum? (true/false, max value, min count, or number of ways).
Recognise it
| Signal in the problem | Why it points here |
|---|---|
| "partition the array into two subsets with equal sum" | Find a subset summing to total/2: 0/1 subset-sum (Partition Equal Subset Sum). |
| "assign + or - to each number to reach target" | Positive group P satisfies P = (total + target) / 2: count subsets (Target Sum). |
| "smash stones, smallest possible remaining weight" | Split into two piles as equal as possible: subset sum closest to total/2 (Last Stone Weight II). |
| "fewest coins to make amount", infinite supply | Unbounded knapsack minimising count (Coin Change). |
| "number of combinations that make up amount" | Unbounded counting, order ignored (Coin Change II). |
| "least number of perfect squares that sum to n" | Squares are coins; unbounded min count (Perfect Squares). |
| "number of possible combinations" where different orders count separately | Ordered counting - really a 1-D sequence DP over the amount (Combination Sum IV). |
| Weights with values and a weight limit | The textbook 0/1 knapsack maximising value. |
Not this pattern if...
- The target is huge (around 10⁹) while n is small: use meet-in-the-middle or backtracking.
- You must list every combination: that is backtracking; DP only counts or optimises.
- Items must be contiguous: that is a sliding window or prefix-sum problem.
- Order of the chosen items matters and they must increase: that is LIS.
Memory hack
"Once? Walk back. Many? Walk forward." In the 1-D array, a 0/1 item loops capacity from high to low so dp[c - w] is still the value before this item. An unbounded item loops low to high so dp[c - w] may already include it.
Second rhyme for counting: "Coins outside = combinations; amount outside = arrangements."
DP framing: state = dp[c], the best result for capacity/sum c; choice = skip the item or take it (dp[c - w] plus its value); base case = dp[0] (true, 0, or 1 way) and everything else "impossible"; order = items in the outer loop, capacity inside in the direction set by reuse.
Analogy: packing a suitcase while checking a cheat-sheet that says, for every smaller suitcase size, the best packing found so far. Each new item only asks, "if I put this in, what is the best packing for the space that remains?"
Say it out loud in the interview:
- "What is the capacity, and is it small enough to index an array?"
- "Can an item be reused? That decides the loop direction."
- "What do I store per capacity - boolean, max, min, or count - and what is dp[0]?"
How it works
For a 0/1 problem, process items one at a time. After processing item k, dp[c] answers the question using only the first k items. Iterating capacity downward guarantees each read of dp[c - w] refers to the previous item layer, so the current item is never counted twice.
Trace for Partition Equal Subset Sum on [1, 5, 11, 5]: total 22, so target 11. Reachable sums after each item:
| Item | Reachable sums (≤ 11) | dp[11]? |
|---|---|---|
| start | 0 | no |
| 1 | 0, 1 | no |
| 5 | 0, 1, 5, 6 | no |
| 11 | 0, 1, 5, 6, 11 | yes |
| 5 | 0, 1, 5, 6, 10, 11 | yes |
The answer is true: {11} versus {1, 5, 5}. Had you looped upward, the first 5 would already have produced 10 by stacking on the 5 it had just created in the same pass - using one item twice. Harmless here by luck, wrong in general.
Java template
import java.util.*; class Knapsack { // 0/1 feasibility: can some subset sum to target? (each item at most once) static boolean subsetSum(int[] nums, int target) { boolean[] dp = new boolean[target + 1]; dp[0] = true; // empty subset for (int x : nums) for (int c = target; c >= x; c--) // DOWNWARD: item used once dp[c] = dp[c] || dp[c - x]; return dp[target]; } // Unbounded minimum count: fewest items (reusable) summing exactly to target, or -1. static int minItems(int[] items, int target) { int INF = target + 1; // larger than any real answer int[] dp = new int[target + 1]; Arrays.fill(dp, INF); dp[0] = 0; for (int w : items) for (int c = w; c <= target; c++) // UPWARD: item reusable dp[c] = Math.min(dp[c], dp[c - w] + 1); return dp[target] >= INF ? -1 : dp[target]; } // Counting. reusable=false -> 0/1 subsets; reusable=true -> combinations (order ignored). static int countWays(int[] items, int target, boolean reusable) { int[] dp = new int[target + 1]; dp[0] = 1; // one way to make 0: take nothing for (int w : items) { if (reusable) for (int c = w; c <= target; c++) dp[c] += dp[c - w]; else for (int c = target; c >= w; c--) dp[c] += dp[c - w]; } return dp[target]; } // Ordered counting (permutations): amount outside, items inside. static int countOrdered(int[] items, int target) { int[] dp = new int[target + 1]; dp[0] = 1; for (int c = 1; c <= target; c++) for (int w : items) if (w <= c) dp[c] += dp[c - w]; return dp[target]; } }
Complexity
- Time: O(n × T), where n is the number of items and T the target. This is pseudo-polynomial: fast when T is small, hopeless when T is around 10⁹.
- Space: O(T) with the 1-D rolling array; O(n × T) if you keep the full 2-D table (needed only to reconstruct which items were chosen).
- Perfect Squares uses √n "coins", so it runs in O(n√n).
Variations
- Subset sum / equal partition: Partition Equal Subset Sum - boolean 0/1 over total/2; bail out early if the total is odd.
- Sign assignment → subset count: Target Sum - transform to counting subsets with sum (total + target) / 2; reject odd or negative values.
- Closest split: Last Stone Weight II - find the largest reachable sum ≤ total/2; answer is total - 2 × that.
- Unbounded min count: Coin Change and Perfect Squares -
min(dp[c], dp[c - w] + 1)with an "infinity" sentinel. - Unbounded combinations: Coin Change II - coins outer, amount inner, upward.
- Ordered combinations: Combination Sum IV - amount outer, items inner.
- Two-capacity knapsack: Ones and Zeroes - a 2-D dp over (zeros, ones), both loops downward.
Common mistakes
Wrong loop direction is the classic bug. Upward iteration in a 0/1 problem silently lets one item be used several times; downward iteration in an unbounded problem forbids reuse. Neither throws an error - the numbers are just wrong.
- Swapping the loop nesting for counting. Items-outer counts combinations; amount-outer counts permutations. Coin Change II and Combination Sum IV differ only here.
- Bad sentinel. Using
Integer.MAX_VALUEas infinity and then adding 1 overflows; usetarget + 1or check before adding. - Wrong base case. Counting needs
dp[0] = 1, min-count needsdp[0] = 0with the rest at infinity, feasibility needsdp[0] = true. - Skipping the transform checks in Target Sum: if
(total + target)is odd or|target| > total, the answer is 0. - Ignoring zeros in counting problems. Each 0 can be in or out, doubling the ways; keep the loop bound
c >= wso c = 0 is processed rather than "optimising" zeros away.
All practice problems
7 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.