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 WW 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 nn items. This gives an unacceptable O(2n)O(2^n) 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 O(nW)O(nW), 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 WW. But it is not polynomial in the length of WW'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 O(n)O(n) additional time. You will explain precisely why this algorithm is efficient for reasonable WW. 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 WW is astronomically large. They are also used when exactness under pruning is required.

Key takeaways
  • The 0/1 knapsack problem picks a subset of items, each used at most once, to maximize value under a weight cap WW.
  • 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 O(nW)O(nW) time and O(nW)O(nW) space (reducible to O(W)O(W) space).
  • Because WW appears in the bound as a number, not as log⁡W\log W, the algorithm is pseudo-polynomial, not truly polynomial.
  • Backtracking through the filled table (or a parent pointer) reconstructs the actual chosen items in O(n)O(n) extra work.
  • The fractional relaxation of this problem is solvable greedily in polynomial time, but the 0/1 constraint makes greedy provably wrong.
The core insight

Because the optimal choice for items 1..i1..i depends on the remaining items only through the leftover capacity, there are just O(nW)O(nW) distinct subproblems, and filling them bottom-up in a table turns an exponential search into an O(nW)O(nW) computation.

The idea

The 0/1 knapsack problem is specified by nn items, each with a positive weight wiw_i and a value viv_i, and a capacity WW. A feasible solution is a subset S⊆{1,…,n}S \subseteq \{1, \dots, n\} with ∑i∈Swi≤W\sum_{i \in S} w_i \le W. The goal is to find a feasible SS maximizing ∑i∈Svi\sum_{i \in S} v_i. 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 O(nlog⁡n)O(n \log n) 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

OPT(i,c)=the maximum value achievable using only items 1,…,i with total weight≤c.OPT(i, c) = \text{the maximum value achievable using only items } 1, \dots, i \text{ with total weight} \le c.

Consider item ii in an optimal solution to OPT(i,c)OPT(i, c). Either item ii is excluded — in which case the optimal value is exactly OPT(i−1,c)OPT(i-1, c) — or item ii is included, which requires wi≤cw_i \le c and leaves an optimal solution to the remaining items with reduced capacity, contributing vi+OPT(i−1,c−wi)v_i + OPT(i-1, c - w_i). 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 OPT(i−1,c′)OPT(i-1, c') for a fixed c′c' 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 dp[i][c]dp[i][c] for i=0,…,ni = 0, \dots, n and c=0,…,Wc = 0, \dots, W.

Recurrence.

dp[i][c]={0if i=0 (base case: no items)dp[i−1][c]if wi>c (item i cannot fit)max⁡(dp[i−1][c],  vi+dp[i−1][c−wi])otherwisedp[i][c] = \begin{cases} 0 & \text{if } i = 0 \text{ (base case: no items)} \\ dp[i-1][c] & \text{if } w_i > c \text{ (item } i \text{ cannot fit)} \\ \max\big(dp[i-1][c],\; v_i + dp[i-1][c-w_i]\big) & \text{otherwise} \end{cases}

The base case dp[0][c]=0dp[0][c] = 0 for all cc encodes "zero items chosen, zero value, any capacity is trivially respected." The final answer is dp[n][W]dp[n][W]. Filling the table row by row (increasing ii), 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 ii must be fully computed before row i+1i+1 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 dp[i][c]≠dp[i−1][c]dp[i][c] \ne dp[i-1][c] can only happen when item ii was included (excluding it would have reproduced dp[i−1][c]dp[i-1][c] 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.

Figure
The DP table: each cell depends only on two cells directly above it
dp[i][c] table — items 1..4, capacity 0..7 c=0c=1c=2 c=3c=4c=5 c=6c=7 i=0 i=1 (w=2,v=3) i=2 (w=3,v=4) i=3 (w=4,v=5) i=4 (w=5,v=6) 000 000 00 003 333 33 003 447 77 003 457 89 003 457 89 green = dp[i-1][c] and dp[i-1][c-w_i], the two cells dp[4][7]=9 is derived from answer dp[4][7] = 9 (take items 1 and 3: value 3+5=8... reconstruction shown below finds the true optimum)

Complexity & correctness

Time complexity. The table has (n+1)(W+1)(n+1)(W+1) cells, and each cell is computed in O(1)O(1) time by looking at two already-computed entries. Hence the total running time is

O(nW).O(nW).

Reconstruction walks from row nn to row 11 once, adding O(n)O(n) time, which does not change the asymptotic bound.

Space complexity. The full table uses O(nW)O(nW) space. If only the optimal value is needed (not the item set), the table can be compressed to a single 1D array of size W+1W+1, updated in place scanning capacity from high to low within each item's iteration — this reduces space to O(W)O(W) 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 nn numbers for weights, nn numbers for values, and the capacity WW, all typically encoded in binary. The size of this encoding is roughly O(nlog⁡W)O(n \log W) bits, not O(n+W)O(n + W). An algorithm is polynomial-time if its running time is polynomial in the bit-length of the input. Here the running time O(nW)O(nW) is polynomial in the numeric value WW, but WW can require only log⁡2W\log_2 W bits to write down — so if WW is, say, 2642^{64}, 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 ii). Claim: for every ii and cc, dp[i][c]dp[i][c] equals OPT(i,c)OPT(i,c) as defined above. Base case (i=0i=0): with no items available, the best achievable value under any capacity is 00, matching dp[0][c]=0dp[0][c]=0. Inductive step: assume dp[i−1][c′]=OPT(i−1,c′)dp[i-1][c'] = OPT(i-1,c') for all c′c'. Fix cc. Any feasible solution using items 1,…,i1,\dots,i either omits item ii, in which case its value is at most OPT(i−1,c)=dp[i−1][c]OPT(i-1,c) = dp[i-1][c], or includes item ii (requiring wi≤cw_i \le c), in which case the remaining items form a feasible solution to capacity c−wic-w_i, so the total value is at most vi+OPT(i−1,c−wi)=vi+dp[i−1][c−wi]v_i + OPT(i-1,c-w_i) = v_i + dp[i-1][c-w_i]. 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 dp[i][c]dp[i][c] equals the true optimum OPT(i,c)OPT(i,c). By induction this holds for all ii, in particular dp[n][W]=OPT(n,W)dp[n][W] = OPT(n,W), the answer to the original problem. ■\blacksquare

Why reconstruction is correct. At each step, dp[i][c]≠dp[i−1][c]dp[i][c] \ne dp[i-1][c] can only occur if item ii contributed to the optimum at (i,c)(i,c): the recurrence sets dp[i][c]=dp[i−1][c]dp[i][c] = dp[i-1][c] whenever item ii is excluded or doesn't fit, so any difference forces the "include" branch to have been the maximizer, meaning item ii is in some optimal solution reachable at that cell. Moving to c−wic - w_i correctly continues the search among the remaining items, so the walk-back invariant (the cell (i,c)(i,c) visited always still represents an unresolved optimum) is preserved at every step.

Tradeoffs. The DP is exact and simple, but useless when WW 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-n⋅max⁡(vi)n \cdot \max(v_i) "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 n=4n=4 items with capacity W=7W=7:

item iiweight wiw_ivalue viv_i
123
234
345
456

Filling dp[i][c]dp[i][c] for c=0,…,7c = 0,\dots,7:

i\ci \backslash c01234567
000000000
1 (w2,v3)00333333
2 (w3,v4)00344777
3 (w4,v5)00345789
4 (w5,v6)00345789

Sample cell derivations:

  • dp[2][5]dp[2][5]: item 2 has w2=3≤5w_2=3 \le 5, so dp[2][5]=max⁡(dp[1][5], 4+dp[1][2])=max⁡(3, 4+3)=7dp[2][5] = \max(dp[1][5],\, 4 + dp[1][2]) = \max(3,\, 4+3) = 7. (Take items 1 and 2: weight 2+3=52+3=5, value 3+4=73+4=7.)
  • dp[3][6]dp[3][6]: item 3 has w3=4≤6w_3=4 \le 6, so dp[3][6]=max⁡(dp[2][6], 5+dp[2][2])=max⁡(7, 5+3)=8dp[3][6] = \max(dp[2][6],\, 5 + dp[2][2]) = \max(7,\, 5+3) = 8.
  • dp[4][7]dp[4][7]: item 4 has w4=5≤7w_4=5 \le 7, so dp[4][7]=max⁡(dp[3][7], 6+dp[3][2])=max⁡(9, 6+3)=9dp[4][7] = \max(dp[3][7],\, 6 + dp[3][2]) = \max(9,\, 6+3) = 9. Since 9=dp[3][7]9 = dp[3][7], item 4 is not taken at the final cell.

Answer: dp[4][7]=9dp[4][7] = 9.

Reconstruction, walking backward from (i=4,c=7)(i=4, c=7):

stepcelldp[i][c]dp[i][c] vs dp[i−1][c]dp[i-1][c]decisionnew cc
1(4,7)9=99 = 9item 4 not taken7
2(3,7)9≠dp[2][7]=79 \ne dp[2][7]=7item 3 taken7−4=37-4=3
3(2,3)4≠dp[1][3]=34 \ne dp[1][3]=3item 2 taken3−3=03-3=0
4(1,0)0=dp[0][0]=00 = dp[0][0]=0item 1 not taken0

Chosen items: {3,2}\{3, 2\}, weight 4+3=7≤74+3=7 \le 7, value 5+4=95+4=9 — matching dp[4][7]dp[4][7] 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 (3/2=1.53/2=1.5) first, then item 2 (4/3≈1.334/3\approx1.33), filling capacity 2+3=52+3=5 with value 7 and no room left for anything of weight ≥3\ge 3 — strictly worse than the DP's optimal 99. This gap is the standard demonstration that greedy fails for 0/1 knapsack even though it succeeds for the fractional relaxation.

Common pitfalls & traps

PitfallWhy it happensFix
Confusing 0/1 with fractional knapsackGreedy ratio-sorting is correct for fractional but not 0/1Verify whether items are divisible; use DP only for the indivisible (0/1) case
Off-by-one in capacity indexingForgetting the c=0c=0 column or using 1-indexed weights against 0-indexed arrayAlways allocate `dp[0..n][0..W]` explicitly, test with W=0W=0
In-place 1D optimization loses reconstruction infoOverwriting the array destroys the previous row needed to trace back choicesKeep the full 2D table (or a separate "which item" array) whenever the chosen set is required
Iterating capacity in the wrong direction during 1D compressionThe 1D "rolling array" trick needs high-to-low updates per item to avoid reusing an item twiceScan cc from WW down to wiw_i when compressing to one row
Treating O(nW)O(nW) as truly polynomialWW can be exponential in the number of bits used to encode itState the bound honestly as pseudo-polynomial; use scaling/FPTAS or Branch and Bound for huge WW
Assuming weights or values can be zero/negative without adjustmentZero-weight items can be taken "for free," and the recurrence's max may behave unexpectedly with negative valuesExplicitly define behavior for wi=0w_i=0 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
RE
Richard E. Bellman
1920–1984 · Applied mathematician at RAND Corporation who formulated dynamic programming and the principle of optimality underlying the knapsack recurrence

History
1953
Bellman formalizes dynamic programming and the principle of optimality at RAND Corporation.
1957
Bellman's book "Dynamic Programming" popularizes multistage resource-allocation problems, the direct ancestor of the knapsack DP.
1972
Richard Karp's list of NP-complete problems highlights subset-sum and related combinatorial optimization problems.
1975
Sahni and Ibarra develop fully polynomial-time approximation schemes (FPTAS) for knapsack-type problems via value scaling.
1990
s–present: Knapsack DP becomes a standard teaching example bridging dynamic programming and computational complexity in algorithms courses.

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 O(nW)O(nW) 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 WW 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 2n2^n candidates, since each item is independently included or excluded. Even for a modest n=50n=50, that is over 101510^{15} 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 2n2^n down to O(nW)O(nW), 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 nn and WW) 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 WW is normally written in binary using only O(log⁡W)O(\log W) bits. The running time O(nW)O(nW) is exponential in log⁡W\log W (since W=2log⁡WW = 2^{\log W}), so if WW is astronomically large relative to its bit-length — say W=260W=2^{60} — 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 dp[i][⋅]dp[i][\cdot] depends only on row dp[i−1][⋅]dp[i-1][\cdot], you can maintain a single 1D array of size W+1W+1 and update it in place, provided you iterate capacity from WW down to wiw_i for each item (to avoid reusing an item's contribution multiple times within the same row). This reduces space to O(W)O(W). 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) WW?" Both problems share the same weakly-NP-hard status and the same pseudo-polynomial O(nW)O(nW) 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 (wi,vi)(w_i, v_i) pair. There is no special casing needed: at row ii, the algorithm simply compares "skip item ii" against "take item ii," 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 0,1,…,W0, 1, \dots, W 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 [0,W][0, W] 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 O(1)O(1) comparison (dp[i][c]dp[i][c] vs. dp[i−1][c]dp[i-1][c]) to decide whether item ii was part of the optimum, then moves to a strictly smaller subproblem. Since there are only nn items, this backward walk takes exactly O(n)O(n) comparisons total, negligible next to the O(nW)O(nW) 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.

Before this

Where this leads