The 0/1 Knapsack problem is one of the most important DP patterns. Unlike coin change (unbounded knapsack), each item can only be used once. This “take it or leave it” decision structure appears in dozens of interview problems.
Problem
Given a list of items with weights and values, and a knapsack with maximum capacity W, find the maximum total value you can carry. Each item can be used at most once.
weights = [1, 3, 4, 5] values = [1, 4, 5, 7] capacity = 7 Maximum value = 9 (items with weight 3 and 4: values 4+5=9)
2D DP Solution
def knapsack_2d(weights: list[int], values: list[int], capacity: int) -> int:
"""
dp[i][w] = max value using first i items with capacity w.
For each item i:
- Skip item: dp[i][w] = dp[i-1][w]
- Take item: dp[i][w] = dp[i-1][w - weight[i]] + value[i] (if weight[i] <= w)
Time: O(n * W), Space: O(n * W)
"""
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
w_i = weights[i - 1]
v_i = values[i - 1]
for w in range(capacity + 1):
# Skip this item
dp[i][w] = dp[i - 1][w]
# Take this item (if it fits)
if w_i int:
"""
Space-optimized 0/1 knapsack. dp[w] = max value with capacity w.
Reverse iteration prevents using the same item twice within one pass.
Time: O(n * W), Space: O(W)
"""
dp = [0] * (capacity + 1)
for i in range(len(weights)):
w_i, v_i = weights[i], values[i]
# Reverse order: ensures we use dp[w - w_i] from the PREVIOUS item's row
for w in range(capacity, w_i - 1, -1):
dp[w] = max(dp[w], dp[w - w_i] + v_i)
return dp[capacity]
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
print(knapsack_1d(weights, values, 7)) # 9
Why Reverse Order Matters
In the 1D version, if you iterate forward (left to right), when you compute dp[w], dp[w - weight[i]] has already been updated in this pass — meaning you could use item i multiple times. Reverse iteration ensures you see the previous row’s values.
# Wrong (unbounded): would allow using same item multiple times
for i in range(len(weights)):
for w in range(weights[i], capacity + 1): # forward
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
# Correct (0/1): each item used at most once
for i in range(len(weights)):
for w in range(capacity, weights[i] - 1, -1): # reverse
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
Common Interview Problems Using 0/1 Knapsack
| Problem | Mapping |
|---|---|
| LeetCode 416: Partition Equal Subset Sum | Can we pick items with total = sum/2? Items are the numbers, capacity = target |
| LeetCode 494: Target Sum | Assign +/- to each number; equivalent to subset sum with target |
| LeetCode 1049: Last Stone Weight II | Split stones into two groups; minimize difference = maximize subset sum |
def can_partition(nums: list[int]) -> bool:
"""
LeetCode 416: Can we partition into two equal-sum subsets?
Equivalent to: can we find a subset with sum = total/2?
"""
total = sum(nums)
if total % 2 != 0:
return False
target = total // 2
dp = [False] * (target + 1)
dp[0] = True # Empty subset sums to 0
for num in nums:
for w in range(target, num - 1, -1): # Reverse for 0/1 knapsack
dp[w] = dp[w] or dp[w - num]
return dp[target]
Knapsack vs Coin Change Comparison
| Property | 0/1 Knapsack | Unbounded Knapsack (Coin Change) |
|---|---|---|
| Item use | At most once | Unlimited |
| Inner loop direction | Reverse (capacity → 0) | Forward (0 → capacity) |
| Classic problem | Partition Equal Subset Sum | Coin Change, Coin Change 2 |
Related DP Problems
- Coin Change — unbounded knapsack variant: iterate forward to allow reuse; vs 0/1 knapsack reverse iteration to prevent reuse
- Longest Common Subsequence — both use 2D DP with the “take it or leave it” framing for each element; understand both tables