The 0/1 Knapsack Problem
A burglar stands over a pile of items, each with a weight and a value. She carries a knapsack that can hold at most pounds. Which items should she take to maximize total value? Each item can be taken whole or not at all. This is the 0/1 Knapsack Problem. It is a foundational example in Introduction to Dynamic Programming. It shows how breaking a problem into overlapping subproblems works. This converts an apparently exponential search into a manageable table-filling computation. The problem matters far beyond thieves and sacks. Cloud schedulers decide which jobs to run under a memory budget. Investors select a subset of projects under a capital constraint. Compilers choose which optimizations to apply under a code-size limit. All of these are instances of 0/1 knapsack. Whenever a system must pick an all-or-nothing subset of discrete options, this exact structure reappears. This happens whenever the goal is to maximize value subject to a single resource limit. The naive approach tries every subset of items. This gives an unacceptable cost. Dynamic programming exploits a key fact. Decisions about items interact only through how much capacity remains. So the number of distinct subproblems is only , not exponential. This article builds the recurrence from first principles. It walks through the classic weight-capacity table. It shows how to reconstruct the actual set of chosen items, not just the optimal value. It is also honest about the catch. The running time is pseudo-polynomial. This means it is polynomial in the numeric value of . But it is not polynomial in the length of 's binary representation. By the end you will be able to state and prove the DP recurrence. You will fill the table by hand. You will recover the optimal item set in additional time. You will explain precisely why this algorithm is efficient for reasonable . You will also see why it breaks down when weights are huge. The discussion also sets up two more advanced techniques. These are Backtracking and State-Space Search and Branch and Bound. They handle knapsack-like problems when is astronomically large. They are also used when exactness under pruning is required.
- The 0/1 knapsack problem picks a subset of items, each used at most once, to maximize value under a weight cap .
- The key DP recurrence considers, for each item, only two choices: exclude it, or include it and recurse on reduced capacity.
- A 2D table indexed by (item count, capacity) fills in time and space (reducible to space).
- Because appears in the bound as a number, not as , the algorithm is pseudo-polynomial, not truly polynomial.
- Backtracking through the filled table (or a parent pointer) reconstructs the actual chosen items in extra work.
- The fractional relaxation of this problem is solvable greedily in polynomial time, but the 0/1 constraint makes greedy provably wrong.
Because the optimal choice for items depends on the remaining items only through the leftover capacity, there are just distinct subproblems, and filling them bottom-up in a table turns an exponential search into an computation.
The idea
The 0/1 knapsack problem is specified by items, each with a positive weight and a value , and a capacity . A feasible solution is a subset with . The goal is to find a feasible maximizing . The name "0/1" emphasizes that each item is either fully included (1) or fully excluded (0) — there is no fractional taking, which distinguishes this from the fractional knapsack problem that a greedy exchange argument solves exactly in time. The central design insight, shared with all dynamic programming, is optimal substructure: an optimal solution to the whole problem is built from optimal solutions to smaller subproblems. Define
Consider item in an optimal solution to . Either item is excluded — in which case the optimal value is exactly — or item is included, which requires and leaves an optimal solution to the remaining items with reduced capacity, contributing . Because these are the only two possibilities and each is itself optimal on a strictly smaller subproblem, taking the better of the two gives the recurrence used below. This is the overlapping subproblems property at work: the subproblem for a fixed is reused by many different choices made at later items, so memoizing it (per Memoization versus Tabulation) avoids recomputation.
How it works
The recurrence has two indices — the number of items considered and the remaining capacity — so the natural tabulation strategy is a 2D array for and .
Recurrence.
The base case for all encodes "zero items chosen, zero value, any capacity is trivially respected." The final answer is . Filling the table row by row (increasing ), each cell needs only two entries from the previous row, so a straightforward double loop computes the whole table bottom-up, exactly the tabulation style introduced when contrasting it with top-down memoization. ``` function KNAPSACK(weights[1..n], values[1..n], W): // dp[i][c] = best value using items 1..i with capacity c let dp[0..n][0..W] be a new array, all initialized to 0 for i = 1 to n: for c = 0 to W: if weights[i] > c: dp[i][c] = dp[i-1][c] // item i cannot fit else: dp[i][c] = max(dp[i-1][c], values[i] + dp[i-1][c - weights[i]]) return dp[n][W], dp // keep dp for reconstruction function RECONSTRUCT(dp, weights[1..n], values[1..n], W, n): chosen = empty list c = W for i = n downto 1: if dp[i][c] != dp[i-1][c]: // item i's inclusion strictly improved the value at (i, c), // so item i must have been taken chosen.append(i) c = c - weights[i] // else dp[i][c] == dp[i-1][c]: item i was not taken, c unchanged return chosen // list of item indices in the optimal set ``` Two correctness details deserve emphasis. First, the loop order matters only in that row must be fully computed before row is touched — each row depends solely on the row directly above it, never on itself, so there is no ordering ambiguity within a row. Second, the reconstruction loop works backward precisely because can only happen when item was included (excluding it would have reproduced exactly); this is the standard technique of walking parent choices back through a DP table, foreshadowing the explicit choice-tracking used in Backtracking and State-Space Search.
Complexity & correctness
Time complexity. The table has cells, and each cell is computed in time by looking at two already-computed entries. Hence the total running time is
Reconstruction walks from row to row once, adding time, which does not change the asymptotic bound.
Space complexity. The full table uses space. If only the optimal value is needed (not the item set), the table can be compressed to a single 1D array of size , updated in place scanning capacity from high to low within each item's iteration — this reduces space to but destroys the information needed for reconstruction, since overwriting a row erases the previous row. Keeping the full 2D table (or, more space-efficiently, storing only which item was taken at each cell) is the standard tradeoff when the actual item set is required.
Why "pseudo-polynomial." The input to the problem consists of numbers for weights, numbers for values, and the capacity , all typically encoded in binary. The size of this encoding is roughly bits, not . An algorithm is polynomial-time if its running time is polynomial in the bit-length of the input. Here the running time is polynomial in the numeric value , but can require only bits to write down — so if is, say, , the table has more cells than atoms in reach, even though the input itself is tiny. This gap between "polynomial in the numbers" and "polynomial in the input size" is exactly the definition of a pseudo-polynomial algorithm, and 0/1 knapsack is the textbook example of the concept. (Indeed 0/1 knapsack is NP-hard in the strong sense's opposite: it is only weakly NP-hard, precisely because this pseudo-polynomial algorithm exists — a strongly NP-hard problem admits no pseudo-polynomial algorithm unless P = NP.)
Correctness proof sketch (induction on ). Claim: for every and , equals as defined above. Base case (): with no items available, the best achievable value under any capacity is , matching . Inductive step: assume for all . Fix . Any feasible solution using items either omits item , in which case its value is at most , or includes item (requiring ), in which case the remaining items form a feasible solution to capacity , so the total value is at most . Since these two cases are exhaustive and each bound is achieved by some solution (by the inductive hypothesis, the subproblems attain their optima), taking the max of the two bounds is both an upper bound on every feasible solution and achievable — hence equals the true optimum . By induction this holds for all , in particular , the answer to the original problem.
Why reconstruction is correct. At each step, can only occur if item contributed to the optimum at : the recurrence sets whenever item is excluded or doesn't fit, so any difference forces the "include" branch to have been the maximizer, meaning item is in some optimal solution reachable at that cell. Moving to correctly continues the search among the remaining items, so the walk-back invariant (the cell visited always still represents an unresolved optimum) is preserved at every step.
Tradeoffs. The DP is exact and simple, but useless when is huge (a common case in real capital-budgeting or bin-packing-adjacent settings). In that regime, one falls back on the pseudo-polynomial-in- "value-indexed" DP, approximation schemes (a fully polynomial-time approximation scheme, FPTAS, exists by scaling values), or exponential-but-pruned search such as Branch and Bound.
Worked example
Consider items with capacity :
| item | weight | value |
|---|---|---|
| 1 | 2 | 3 |
| 2 | 3 | 4 |
| 3 | 4 | 5 |
| 4 | 5 | 6 |
Filling for :
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 (w2,v3) | 0 | 0 | 3 | 3 | 3 | 3 | 3 | 3 |
| 2 (w3,v4) | 0 | 0 | 3 | 4 | 4 | 7 | 7 | 7 |
| 3 (w4,v5) | 0 | 0 | 3 | 4 | 5 | 7 | 8 | 9 |
| 4 (w5,v6) | 0 | 0 | 3 | 4 | 5 | 7 | 8 | 9 |
Sample cell derivations:
- : item 2 has , so . (Take items 1 and 2: weight , value .)
- : item 3 has , so .
- : item 4 has , so . Since , item 4 is not taken at the final cell.
Answer: .
Reconstruction, walking backward from :
| step | cell | vs | decision | new |
|---|---|---|---|---|
| 1 | (4,7) | item 4 not taken | 7 | |
| 2 | (3,7) | item 3 taken | ||
| 3 | (2,3) | item 2 taken | ||
| 4 | (1,0) | item 1 not taken | 0 |
Chosen items: , weight , value — matching exactly, and confirming the table's claim. Note that the greedy heuristic "sort by value-to-weight ratio and take greedily" would pick item 1 () first, then item 2 (), filling capacity with value 7 and no room left for anything of weight — strictly worse than the DP's optimal . This gap is the standard demonstration that greedy fails for 0/1 knapsack even though it succeeds for the fractional relaxation.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Confusing 0/1 with fractional knapsack | Greedy ratio-sorting is correct for fractional but not 0/1 | Verify whether items are divisible; use DP only for the indivisible (0/1) case |
| Off-by-one in capacity indexing | Forgetting the column or using 1-indexed weights against 0-indexed array | Always allocate `dp[0..n][0..W]` explicitly, test with |
| In-place 1D optimization loses reconstruction info | Overwriting the array destroys the previous row needed to trace back choices | Keep the full 2D table (or a separate "which item" array) whenever the chosen set is required |
| Iterating capacity in the wrong direction during 1D compression | The 1D "rolling array" trick needs high-to-low updates per item to avoid reusing an item twice | Scan from down to when compressing to one row |
| Treating as truly polynomial | can be exponential in the number of bits used to encode it | State the bound honestly as pseudo-polynomial; use scaling/FPTAS or Branch and Bound for huge |
| Assuming weights or values can be zero/negative without adjustment | Zero-weight items can be taken "for free," and the recurrence's max may behave unexpectedly with negative values | Explicitly define behavior for items and disallow or handle negative values |
Pioneers & history
The knapsack problem's formal study is inseparable from the birth of dynamic programming itself. Richard Bellman, who coined the term "dynamic programming" in the 1950s while working at RAND Corporation, developed the principle of optimality — that an optimal policy consists of optimal sub-policies — which is exactly the substructure argument used above. Knapsack-style allocation problems appeared naturally in Bellman's work on multistage resource-allocation, and the specific DP formulation over weight-capacity was popularized through operations research literature in the following decades. The complexity-theoretic classification of the problem — NP-hard, yet only weakly so, admitting a pseudo-polynomial algorithm and an FPTAS — was cemented once the theory of NP-completeness matured in the 1970s, following the foundational work of Stephen Cook on NP-completeness itself. Karp's 1972 list of 21 NP-complete problems included the closely related subset-sum problem, cementing knapsack-family problems as central examples in complexity theory courses ever since.
Origins · 1 minRichard E. Bellman and how this was discovered
Where this matters
Cloud resource schedulers use knapsack-style reasoning to decide which containers or virtual machines to pack onto a physical host without exceeding memory or CPU capacity. Portfolio and capital-budgeting software selects a subset of candidate projects to fund under a fixed budget, directly mapping projects to items and cost to weight. Compilers and embedded-systems toolchains apply knapsack formulations when choosing which optional code transformations to enable under a strict code-size or instruction-count limit. Bioinformatics pipelines use knapsack-adjacent DP when selecting a subset of experiments or reads under a sequencing-budget constraint. In every case, the same three ingredients recur: discrete either/or choices, a single linear resource constraint, and a separable value to maximize — and the table-filling technique, together with its reconstruction step, is the standard first tool reached for before escalating to heavier machinery like Branch and Bound or approximation schemes when grows too large.
:::quiz[What is the base case of the knapsack DP recurrence dp[i][c]?]
- dp[n][W] = 0
- dp[0][c] = 0 for all c ✓
- dp[i][0] = infinity
- dp[i][c] = values[i] for all i
Ask the tutor: How would the recurrence and reconstruction change if each item could be taken multiple times (the unbounded knapsack variant)?
Why can't we just check all subsets of items?
Checking all subsets requires evaluating candidates, since each item is independently included or excluded. Even for a modest , that is over subsets — far beyond feasible computation. The dynamic programming approach exploits the fact that the only information later decisions need about earlier items is the remaining capacity, not the full history of which specific items were taken. This collapses the effective number of distinct subproblems from down to , because a subproblem is identified by just two numbers (item index, remaining capacity) rather than by an entire subset. That reduction from exponentially many states to polynomially many (in and ) is precisely what makes dynamic programming powerful here.
Is the O(nW) algorithm truly polynomial-time?
No — it is pseudo-polynomial. A genuinely polynomial-time algorithm must run in time polynomial in the length of the input encoding, and is normally written in binary using only bits. The running time is exponential in (since ), so if is astronomically large relative to its bit-length — say — the algorithm becomes completely impractical despite the input being tiny. This is exactly why 0/1 knapsack is classified as only weakly NP-hard: it admits a pseudo-polynomial algorithm, whereas strongly NP-hard problems admit no such algorithm unless P = NP.
Can we reduce the space usage below O(nW)?
Yes, if you only need the optimal value and not the chosen items. Because each row depends only on row , you can maintain a single 1D array of size and update it in place, provided you iterate capacity from down to for each item (to avoid reusing an item's contribution multiple times within the same row). This reduces space to . However, this destroys the historical rows needed to reconstruct which specific items were chosen, so if you need the actual subset, you must either keep the full 2D table or separately record, for each cell, whether the corresponding item was taken.
How does 0/1 knapsack relate to the subset-sum problem?
Subset-sum is the special case where each item's value equals its weight, and the question becomes "can we select a subset with total weight exactly (or at most) ?" Both problems share the same weakly-NP-hard status and the same pseudo-polynomial dynamic programming solution, using a Boolean table instead of a value table. Subset-sum is often taught first because its recurrence is simpler (true/false rather than a max), making it a natural stepping stone before tackling the value-maximization version of knapsack.
What happens if two items have the same weight but different values?
The DP recurrence handles this automatically and correctly, since each item is processed independently in its own row of the table using its own pair. There is no special casing needed: at row , the algorithm simply compares "skip item " against "take item ," regardless of whether some other item happens to share its weight. Ties in weight only matter if you care about which specific optimal solution is returned when multiple subsets achieve the same maximum value — the reconstruction procedure returns one particular optimal set, not necessarily all of them.
Does this DP work if weights are not integers?
The table-indexing approach fundamentally relies on capacity values being enumerable as discrete array indices, which requires integer (or rational, after scaling to a common denominator) weights and capacity. If weights are irrational or arbitrary real numbers, the state space is uncountable and no finite table can cover it; in that regime one typically falls back to the fractional relaxation (solvable by greedy ratio-sorting) or to continuous optimization techniques rather than this discrete DP. In practice, weights are often already integers (kilograms, byte counts, cost units), or can be scaled and rounded to integers for an approximate solution.
How does reconstruction avoid re-solving the whole problem from scratch?
Reconstruction reuses the already-computed table entries rather than recomputing anything: at each step it performs a single comparison ( vs. ) to decide whether item was part of the optimum, then moves to a strictly smaller subproblem. Since there are only items, this backward walk takes exactly comparisons total, negligible next to the cost of building the table in the first place. This is the same "trace back through memoized choices" pattern used broadly across dynamic programming whenever the actual solution structure, not just its value, is required.