GeeksforGeeks Medium

0/1 Knapsack

0/1 Knapsack: given item weights and values and a knapsack capacity, maximise the total value carried. Each item may be taken at most once.

Constraints
  • 1 <= n <= 10³
  • 1 <= W <= 10³
  • 1 <= value[i], weight[i] <= 10³
  • Each item may be taken at most once
dpknapsack
Open on GeeksforGeeks ↗
02

Intuition

The 0 1 knapsack problem gives item weights and values plus a capacity, and asks for the maximum value you can carry, taking each item at most once. That last restriction is the whole difficulty. The natural first instinct is greedy by value-to-weight ratio, and it is wrong here. It works for the fractional version, where you can take part of an item and fill the sack exactly. With indivisible items the capacity must be packed as a whole, and an item with a slightly worse ratio may fit the remaining space perfectly where the better one leaves a gap. So every item poses one binary question — take it or skip it — and both branches have to be explored. Skipping leaves the capacity untouched; taking it consumes weight and earns value: - dp[i][c] = max(dp[i−1][c], value[i] + dp[i−1][c − weight[i]]) The detail that enforces "at most once" is that both branches read row i−1, never row i. An item can never consult a state that already includes itself, so it cannot be taken twice. That is also what dictates the space optimisation. Collapsing to one row works, but the inner loop must run backwards over capacity — iterating forward would let dp[c − weight] already include the current item, silently turning this into unbounded knapsack. Rod cutting is that other problem, and the two differ by loop direction alone.

How to spot this pattern

The template every take-or-skip DP is built from. Each item is used at most once, so the state is (items considered, capacity remaining) and each cell picks the better of two options. Recognising the 0/1 shape — indivisible items, one use each — is what tells you greedy will fail and a table is required. It is the same reason the 0/1 knapsack problem using greedy method is taught as a counterexample rather than a solution, and why exact alternatives like the 0/1 knapsack problem using branch and bound prune the search tree instead of pretending a ratio ordering is optimal. Courses that set it as the 0/1 knapsack problem in daa use exactly this contrast.

03

Approach

Try it first

Before reading on: price up what the direct approach costs here, then ask what a single cell should mean, and which earlier cells it needs. Aim for O(n · capacity) time and O(n · capacity) space.

1

See why greedy fails

Value-to-weight ratio is optimal for fractional knapsack but not here. Indivisible items mean capacity must be packed exactly, so a worse-ratio item that fits the remaining gap can beat a better one that leaves it empty.

2

Define the two-dimensional state

dp[i][c] is the best value using only the first i items within capacity c. Row 0 is all zeros — with no items available, no value is achievable at any capacity.

3

Take the better of skip and take

Skipping gives dp[i-1][c]. Taking gives value[i] + dp[i-1][c - weight[i]], but only when the item fits. If it does not fit, skipping is the only option and no maximum is taken.

4

Understand why both branches read the previous row

Neither branch consults row i, so an item can never be counted twice. This is exactly what enforces the at-most-once rule — the structure of the recurrence, not a separate check.

5

Collapse to one row carefully

A single array over capacity suffices, but the inner loop must run backwards. Iterating forward lets dp[c - weight] already include the current item, which silently converts the problem into unbounded knapsack.

6

Contrast with unbounded knapsack

Rod cutting and coin change allow unlimited copies and sweep the capacity forward for exactly that reason. The two problems differ by loop direction alone, which is why copying the wrong template is such a common error.

7

Cost of the tabulation

Every one of the n × capacity cells is computed once, giving O(n · capacity) time with O(capacity) space after the rolling-array optimisation. This is pseudo-polynomial — it scales with the numeric capacity, not the input size.

04

Solution & live demo

▶1class Solution:
▶2 def knapsack(self, wt, val, cap):
▶3 n = len(wt)
▶4 dp = [[0] * (cap + 1) for _ in range(n + 1)]
▶5 for i in range(1, n + 1):
▶6 for c in range(cap + 1):
▶7 skip = dp[i - 1][c]
▶8 if wt[i - 1] <= c:
▶9 take = val[i - 1] + dp[i - 1][c - wt[i - 1]]
▶10 dp[i][c] = max(skip, take)
▶11 else:
▶12 dp[i][c] = skip
▶13 return dp[n][cap]
05

Common pitfalls

Sorting by value-to-weight ratio and taking greedily

✗ Wrong
items.sort(key=lambda x: x[0] / x[1], reverse=True)
for v, w in items:
    if cap >= w: total += v; cap -= w
✓ Right
dp[i][c] = max(skip, take)

That's the fractional knapsack solution, and it fails here: with capacity 4 and items (value 3, weight 3) and (value 4, weight 4), the better ratio takes the 3 and wastes a unit. Indivisible items break the exchange argument that makes greedy safe.

Indexing weights with the table index

✗ Wrong
if wt[i] <= c:
    take = val[i] + dp[i - 1][c - wt[i]]
✓ Right
if wt[i - 1] <= c:
    take = val[i - 1] + dp[i - 1][c - wt[i - 1]]

Row i means "the first i items", so the item it just added sits at array position i - 1. Using i directly reads the next item and runs off the end on the last row.

Considering take when the item doesn't fit

✗ Wrong
take = val[i-1] + dp[i-1][c - wt[i-1]]
dp[i][c] = max(skip, take)
✓ Right
if wt[i - 1] <= c:
    dp[i][c] = max(skip, take)
else:
    dp[i][c] = skip

A negative capacity index wraps to the far end of the row in Python, silently mixing in a value from an unrelated state. An item heavier than the remaining capacity has exactly one option — skip.

06

Edge cases

capacity = 0

Column 0 stays 0 — nothing fits, so no value is achievable.

Item heavier than the whole knapsack

weight > c in every column, so the value is inherited unchanged from the row above.

All items fit together

Every take branch wins and the answer is the sum of all values.

07

Complexity

Time
O(n · capacity)
Space
O(n · capacity)
Pseudo-polynomial: linear in the capacity's value, exponential in the bits used to write it. Rolling to one row gives O(capacity) space.