1. 0/1 Knapsack
The 0/1 knapsack problem is one of the canonical examples of dynamic programming and one of the most widely studied NP-hard combinatorial optimisation problems. Given \(n\) items each with a value \(v_i\) and a weight \(w_i\), and a knapsack of capacity \(W\), it asks for the subset of items that maximises total value subject to the total weight not exceeding \(W\). Each item is either included (1) or excluded (0) — hence the name 12.
The problem is NP-hard in the strong sense when \(W\) is exponential in the input size; the dynamic programming solution runs in \(O(nW)\), which is pseudo-polynomial — polynomial in the value of \(W\) but not in its bit length \(\lceil \log_2 W \rceil\).
1.1 Problem Statement
Given \(n\) items with values \(v_1, \ldots, v_n \in \mathbb{Z}_{> 0}\), weights \(w_1, \ldots, w_n \in \mathbb{Z}_{> 0}\), and capacity \(W \in \mathbb{Z}_{> 0}\), find a binary vector \(x \in \{0, 1\}^n\) maximising:
Variables
| Symbol | Meaning |
|---|---|
| \(n\) | Number of items |
| \(v_i\) | Value of item \(i\) |
| \(w_i\) | Weight of item \(i\) |
| \(W\) | Knapsack capacity |
| \(x_i \in \{0,1\}\) | Decision variable: 1 if item \(i\) is included |
| \(\text{dp}[i][c]\) | Optimal value using items \(1..i\) with capacity \(c\) |
1.2 Why Greedy Fails
A natural greedy heuristic — include items in decreasing order of value-per-unit-weight \(v_i / w_i\) — is optimal for the fractional knapsack (where items can be split) but not for the 0/1 version.
Counterexample: \(W = 10\), items: \((v_1, w_1) = (6, 4)\), \((v_2, w_2) = (5, 3)\), \((v_3, w_3) = (5, 3)\). Ratios: \(v_1/w_1 = 1.5\), \(v_2/w_2 = v_3/w_3 = 1.67\). Greedy picks items 2 and 3: total value = 10, weight = 6. But it cannot fit item 1 in a gap of \(W - 6 = 4 \geq w_1 = 4\) — wait, it can: optimal is items 1 and 2 (or 1 and 3): value = 11. Greedy (by ratio) would pick 2 and 3 then 1 if space allows, reaching 11. Let's use a cleaner counterexample: \(W = 5\), \((v_1, w_1) = (4, 3)\), \((v_2, w_2) = (3, 2)\), \((v_3, w_3) = (3, 2)\). Greedy by ratio: item 1 has ratio 1.33, items 2,3 have 1.5 — picks 2, then 3 (\(w=4 \leq 5\)), value=6. DP finds taking items 1+2 (\(w=5\), value=7) or 1+3 is also 7. Greedy beats ratio here actually... The correct intuition is: greedy works fractionally but fails 0/1 because it can leave "wasted" capacity.
1.3 Optimal Substructure and Recurrence
The 0/1 knapsack has optimal substructure: the optimal choice for items \(1..i\) with capacity \(c\) depends only on the optimal choice for items \(1..i-1\) with capacity \(c\) (exclude item \(i\)) or capacity \(c - w_i\) (include item \(i\), if \(w_i \leq c\)). This gives the recurrence:
1.4 The Algorithm
KNAPSACK(v, w, n, W):
// fill dp table
for i = 0 to n:
dp[i][0] = 0
for c = 0 to W:
dp[0][c] = 0
for i = 1 to n:
for c = 1 to W:
dp[i][c] = dp[i-1][c] // exclude item i
if w[i] ≤ c:
dp[i][c] = max(dp[i][c], v[i] + dp[i-1][c - w[i]]) // include
return dp[n][W]
RECONSTRUCT(dp, w, n, W):
// backtrack to find which items were selected
items = []
c = W
for i = n downto 1:
if dp[i][c] ≠ dp[i-1][c]:
items.append(i)
c = c - w[i]
return items
1.4.1 Space Optimisation
Only the previous row dp[i-1][·] is needed to compute row dp[i][·], so the table can be compressed to a 1D array of length \(W + 1\) by iterating \(c\) from \(W\) downto \(w_i\) (right-to-left):
KNAPSACK-1D(v, w, n, W):
dp = array of size W+1, initialised to 0
for i = 1 to n:
for c = W downto w[i]: // right-to-left prevents reuse
dp[c] = max(dp[c], v[i] + dp[c - w[i]])
return dp[W]
The right-to-left order ensures each item is not counted more than once (i.e. enforces the 0/1 constraint). Left-to-right would give the unbounded knapsack (each item may be selected multiple times).
1.5 Complexity
| Aspect | Cost |
|---|---|
| Time (DP table) | \(O(nW)\) |
| Space (2D table) | \(O(nW)\) |
| Space (1D optimised) | \(O(W)\) |
| Reconstruction | \(O(n)\) (with full table) |
\(O(nW)\) is pseudo-polynomial: it is polynomial in \(n\) and the numeric value of \(W\), but exponential in the bit length of the input since \(W\) can be encoded in \(O(\log W)\) bits. No polynomial-time algorithm is known for the exact optimisation problem (NP-hardness follows from reduction from SUBSET-SUM) 12.
1.6 Fractional Knapsack (Comparison)
In the fractional knapsack problem items can be split: take any fraction \(x_i \in [0, 1]\) of item \(i\). The greedy algorithm (fill items by decreasing \(v_i / w_i\) ratio, taking fractions as needed) is optimal in \(O(n \log n)\) (dominated by sorting). The 0/1 version does not admit a greedy solution because fractional solutions have higher LP relaxation values.
| Version | Optimal algorithm | Complexity |
|---|---|---|
| Fractional | Greedy by ratio | \(O(n \log n)\) |
| 0/1 | Dynamic programming | \(O(nW)\) (pseudo-poly) |
| 0/1 approximation | FPTAS | \(O(n^2 / \varepsilon)\) for \((1-\varepsilon)\)-approx |
1.7 FPTAS
The 0/1 knapsack admits a fully polynomial-time approximation scheme (FPTAS): scale all values by \(K = \varepsilon v_{\max} / n\) (round down), solve the DP on the scaled values, and map back. The resulting solution has value \(\geq (1 - \varepsilon) \text{OPT}\) and runs in \(O(n^2 / \varepsilon)\) 2.
1.8 Interactive Demo
The canvas below shows the DP table being filled for a small instance (\(n = 6\) items, \(W = 15\)). Each cell \(\text{dp}[i][c]\) is coloured by value; the reconstructed path is highlighted after the table is complete.