Master all knapsack variants — 0/1 knapsack with 2D and 1D DP tables, unbounded knapsack with reusable items, fractional knapsack with greedy, subset sum, partition equal subset sum, and the general framework for recognizing knapsack problems in interviews.
The Knapsack Problem Family
The knapsack problem is a family of optimization problems where you must select items subject to a capacity constraint to maximize value. Three main variants:
- 0/1 Knapsack: Each item can be taken at most once
- Unbounded Knapsack: Each item can be taken unlimited times
- Fractional Knapsack: Items can be taken partially (solved greedily)
Unbounded Knapsack — Items Can Be Reused
def knapsack_unbounded(weights, values, W):
"""
Unbounded Knapsack: each item can be used any number of times.
dp[w] = max value achievable with capacity exactly w.
Process capacity in FORWARD order to allow item reuse.
Time: O(n×W), Space: O(W)
"""
dp = [0] * (W + 1)
for w in range(1, W+1):
for i in range(len(weights)):
if weights[i] <= w:
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[W]
# Coin change (minimum coins) — related problem:
def coin_change_unbounded(coins, amount):
"""Minimum coins to make amount. Each coin can be used unlimited times."""
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for coin in coins:
if coin <= a and dp[a - coin] + 1 < dp[a]:
dp[a] = dp[a - coin] + 1
return dp[amount] if dp[amount] != float('inf') else -1
print(coin_change_unbounded([1,5,6,9], 11)) # 2 (5+6)
Key difference from 0/1: Forward iteration allows the same item multiple times.
Fractional Knapsack — Greedy Solution
def knapsack_fractional(weights, values, W):
"""
Fractional knapsack: can take fractions of items.
Greedy: always take item with highest value/weight ratio.
Time: O(n log n) for sorting
"""
n = len(weights)
# Create list of (value_per_weight, weight, value)
items = sorted(zip(values, weights), key=lambda x: x[0]/x[1], reverse=True)
total_value = 0
remaining = W
for val, wt in items:
if remaining <= 0: break
if wt <= remaining:
total_value += val # Take whole item
remaining -= wt
else:
total_value += val * (remaining / wt) # Take fraction
remaining = 0
return total_value
print(knapsack_fractional([10, 40, 20, 30], [2, 4, 6, 9], 15))
# 31.0 (take all of items 0,1 and fraction of item 2)
Why greedy works for fractional but NOT 0/1:
- Fractional: if you can take 0.5 of item A (best ratio), it's always worth it
- 0/1: taking item A might prevent taking items B+C which together are more valuable
Common Knapsack Variants
Subset Sum
"Can you select some subset of nums that sums to target?"
def subset_sum(nums, target):
"""
Special case of 0/1 knapsack: all values = 1, find if sum = target achievable.
dp[w] = True if subset summing to w is possible.
"""
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
dp[w] = dp[w] or dp[w - num]
return dp[target]
print(subset_sum([3, 34, 4, 12, 5, 2], 9)) # True (4+3+2=9)
print(subset_sum([3, 34, 4, 12, 5, 2], 30)) # False
Partition Equal Subset Sum
"Can the array be partitioned into two equal-sum subsets?"
def can_partition(nums):
"""
Partition into two equal subsets iff subset_sum(nums, total/2) is True.
"""
total = sum(nums)
if total % 2 != 0: return False
return subset_sum(nums, total // 2)
print(can_partition([1, 5, 11, 5])) # True — {1,5,5} and {11}
print(can_partition([1, 2, 3, 5])) # False
Count Subsets with Sum
def count_subsets(nums, target):
"""Count number of subsets summing to target."""
dp = [0] * (target + 1)
dp[0] = 1 # One way to make sum 0: empty subset
for num in nums:
for w in range(target, num-1, -1):
dp[w] += dp[w - num]
return dp[target]
print(count_subsets([1, 1, 2, 3], 4)) # 3 (subsets: {1,3},{1,3},{2,2} wait let me check)
Minimum Subset Sum Difference
def min_subset_diff(nums):
"""
Split nums into two subsets, minimize |sum1 - sum2|.
"""
total = sum(nums)
target = total // 2
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
for w in range(target, num-1, -1):
dp[w] = dp[w] or dp[w - num]
# Find largest achievable sum ≤ total/2
for s in range(target, -1, -1):
if dp[s]:
return total - 2*s # Difference
print(min_subset_diff([1, 6, 11, 5])) # 1 ({1,5,6} and {11})
Knapsack Problem Framework
Recognizing a knapsack problem:
Is it asking: "Select from items to optimize something subject to a limit?"
→ Knapsack family
Can each item be used unlimited times?
→ YES: Unbounded Knapsack (forward iteration)
→ NO: 0/1 Knapsack (reverse iteration)
Can you take fractions?
→ YES: Fractional Knapsack (greedy by value/weight ratio)
→ NO: Use DP
Is the "value" just 1 (boolean — can we reach exactly X)?
→ Subset Sum variant
Are you counting ways rather than maximizing?
→ Count variant (sum instead of max)
Complexity Summary
| Variant | Time | Space |
|---|
| 0/1 Knapsack (2D) | O(n×W) | O(n×W) |
| 0/1 Knapsack (1D) | O(n×W) | O(W) |
| Unbounded Knapsack | O(n×W) | O(W) |
| Fractional Knapsack | O(n log n) | O(1) |
| Subset Sum | O(n×target) | O(target) |
Summary
The knapsack problem is the most widely recognized DP pattern:
- 0/1 Knapsack: Items used at most once → reverse iteration in 1D
- Unbounded Knapsack: Items used unlimited times → forward iteration
- Fractional Knapsack: Can take fractions → greedy by value density
- Subset Sum: Special knapsack with values=1 → boolean DP
- Partition Equal Subset: Two equal groups → subset sum to total/2
When you see "select items, capacity constraint, maximize/find if possible" → knapsack.
*Next Lesson: Longest Common Subsequence*