Dynamic Programming Knapsack Patterns: 0/1, Unbounded, Subset Sum

Knapsack problems are the most important dynamic programming pattern class. Mastering 0/1 knapsack, unbounded knapsack, and their variants (subset sum, coin change, partition equal subset) unlocks a wide range of interview problems at top tech companies.

0/1 Knapsack: The Template

Problem: given items with weights and values, and capacity W,
         maximize total value without exceeding capacity.
         Each item can be used at most once (0/1).

def knapsack_01(weights: list, values: list, W: int) -> int:
    n = len(weights)
    # dp[i][w] = max value using first i items with capacity w
    dp = [[0] * (W + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        w_i, v_i = weights[i-1], values[i-1]
        for w in range(W + 1):
            # Option 1: skip item i
            dp[i][w] = dp[i-1][w]
            # Option 2: take item i (if it fits)
            if w >= w_i:
                dp[i][w] = max(dp[i][w], dp[i-1][w - w_i] + v_i)
    return dp[n][W]

# Space optimization to O(W): iterate weights in reverse
def knapsack_01_optimized(weights, values, W):
    dp = [0] * (W + 1)
    for w_i, v_i in zip(weights, values):
        for w in range(W, w_i - 1, -1):  # ← REVERSE: prevents using same item twice
            dp[w] = max(dp[w], dp[w - w_i] + v_i)
    return dp[W]

# Time: O(n × W), Space: O(W)

Pattern: Subset Sum

def canPartition(nums: list[int]) -> bool:
    """LC 416: partition array into two equal-sum subsets"""
    total = sum(nums)
    if total % 2 != 0:
        return False
    target = total // 2
    # dp[w] = can we form sum w using some subset?
    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 to avoid reuse
            dp[w] = dp[w] or dp[w - num]
    return dp[target]

# Generalization: find subset summing to target
def subsetSum(nums: list[int], target: int) -> bool:
    dp = [False] * (target + 1)
    dp[0] = True
    for num in nums:
        for w in range(target, num - 1, -1):
            if dp[w - num]:
                dp[w] = True
    return dp[target]

Pattern: Unbounded Knapsack (items reusable)

def knapsack_unbounded(weights, values, W):
    """Each item can be used unlimited times"""
    dp = [0] * (W + 1)
    for w_i, v_i in zip(weights, values):
        for w in range(w_i, W + 1):  # ← FORWARD: allows reusing same item
            dp[w] = max(dp[w], dp[w - w_i] + v_i)
    return dp[W]

# Key insight: reverse loop = 0/1 (each item once)
#              forward loop = unbounded (item reusable)

Pattern: Coin Change (Minimum Coins)

def coinChange(coins: list[int], amount: int) -> int:
    """LC 322: fewest coins to make amount — unbounded knapsack variant"""
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for coin in coins:
        for amt in range(coin, amount + 1):  # forward: coins reusable
            if dp[amt - coin] != float('inf'):
                dp[amt] = min(dp[amt], dp[amt - coin] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1

def coinChangeWays(coins: list[int], amount: int) -> int:
    """LC 518: number of combinations to make amount"""
    dp = [0] * (amount + 1)
    dp[0] = 1  # one way to make 0: use no coins
    for coin in coins:
        for amt in range(coin, amount + 1):
            dp[amt] += dp[amt - coin]
    return dp[amount]

# NOTE: outer loop is coins, inner is amount → each combination counted once
# If outer loop were amount and inner coins → permutations (order matters)

Pattern: Target Sum (LC 494)

def findTargetSumWays(nums: list[int], target: int) -> int:
    """Assign + or - to each num to reach target"""
    # Let P = set of positives, N = set of negatives
    # P - N = target, P + N = total
    # → 2P = target + total → P = (target + total) / 2
    # Count subsets summing to (target + total) // 2
    total = sum(nums)
    if (total + target) % 2 != 0 or abs(target) > total:
        return 0
    subset_target = (total + target) // 2
    dp = [0] * (subset_target + 1)
    dp[0] = 1
    for num in nums:
        for w in range(subset_target, num - 1, -1):
            dp[w] += dp[w - num]
    return dp[subset_target]

Pattern: Last Stone Weight II (LC 1049)

def lastStoneWeightII(stones: list[int]) -> int:
    """Minimize |sum(group1) - sum(group2)| = minimize 2×sum(group1) - total"""
    # Equivalent to: find subset sum closest to total//2
    total = sum(stones)
    target = total // 2
    dp = [False] * (target + 1)
    dp[0] = True
    for s in stones:
        for w in range(target, s - 1, -1):
            dp[w] = dp[w] or dp[w - s]
    # Find largest achievable sum ≤ target
    for s in range(target, -1, -1):
        if dp[s]:
            return total - 2 * s

Pattern: 2D Knapsack (Ones and Zeroes, LC 474)

def findMaxForm(strs: list[str], m: int, n: int) -> int:
    """Largest subset with at most m zeros and n ones — 2D knapsack"""
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for s in strs:
        zeros = s.count('0')
        ones = len(s) - zeros
        # Reverse both dimensions (0/1: each string used once)
        for i in range(m, zeros - 1, -1):
            for j in range(n, ones - 1, -1):
                dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1)
    return dp[m][n]

Pattern: Bounded Knapsack (each item limited count)

def knapsack_bounded(weights, values, counts, W):
    """Each item i can be used at most counts[i] times"""
    dp = [0] * (W + 1)
    for w_i, v_i, c_i in zip(weights, values, counts):
        # Binary grouping: represent count as 1 + 2 + 4 + ... + remainder
        # Reduces to 0/1 knapsack with O(sum(log c_i)) groups
        groups = []
        k = 1
        remaining = c_i
        while k  0:
            groups.append((remaining * w_i, remaining * v_i))
        # Now apply 0/1 knapsack for each group
        for gw, gv in groups:
            for w in range(W, gw - 1, -1):
                dp[w] = max(dp[w], dp[w - gw] + gv)
    return dp[W]

Knapsack Variant Recognition

Problem Signal Variant Loop Direction
“each item used at most once” 0/1 Knapsack Reverse inner loop
“unlimited supply of each item” Unbounded Forward inner loop
“each item limited to k times” Bounded Binary grouping → 0/1
“can we achieve exactly X” Subset Sum (bool) Reverse, OR updates
“how many ways to make X” Count combinations Forward (coins outer)
“minimize coins/items to make X” Coin Change Forward, min updates

Key Interview Tips

  • The one rule to remember: Reverse inner loop for 0/1 (prevents reusing the item in the same pass). Forward inner loop for unbounded (enables reusing the item). This single distinction separates all knapsack variants.
  • Outer loop order matters for counting: Coin change COMBINATIONS (outer = items, inner = amounts) counts each combination once. Coin change PERMUTATIONS (outer = amounts, inner = items) counts each ordering separately.
  • Subset sum as boolean knapsack: Replace max() with OR, and value with 1. The DP recurrence is identical — this mental mapping unlocks LC 416, 494, 1049, and similar problems.
  • Space optimization: The 2D DP table always collapses to a 1D array because dp[i][w] only depends on dp[i-1][w] and dp[i-1][w-w_i]. Start by writing the 2D solution for clarity, then optimize to 1D.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is the key difference between 0/1 knapsack and unbounded knapsack in DP?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The only implementation difference is the direction of the inner loop. In 0/1 knapsack (each item used at most once), the inner loop iterates from W down to the item’s weight: “for w in range(W, weight-1, -1)”. This reverse iteration ensures that when computing dp[w], the value dp[w-weight] reflects the state before the current item was considered, preventing the item from being used more than once. In unbounded knapsack (items reusable), the inner loop iterates forward from the item’s weight to W: “for w in range(weight, W+1)”. The forward direction means dp[w-weight] already includes the current item, allowing it to be used again.”
}
},
{
“@type”: “Question”,
“name”: “How do you recognize that a problem is a subset sum or knapsack variant?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Knapsack/subset sum problems share these signals: you’re selecting from a set of items (or numbers) to achieve a target, each item can be used a limited number of times, and you’re optimizing or counting. “Partition array into equal sums” u2192 subset sum (target = total/2). “Minimum coins to make amount” u2192 unbounded knapsack (minimize count). “Number of ways to make change” u2192 unbounded knapsack (count combinations). “Assign +/- to reach target” u2192 subset sum (convert to: find subset summing to (total+target)/2). “At most K of each item” u2192 bounded knapsack. The boolean dp (can we achieve X?) uses OR updates; the count dp uses sum updates; the optimization dp uses min/max updates.”
}
},
{
“@type”: “Question”,
“name”: “What is the time and space complexity of the knapsack DP, and how can space be optimized?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The naive 2D knapsack has O(n u00d7 W) time and O(n u00d7 W) space, where n is the number of items and W is the capacity. Space is optimized to O(W) by observing that dp[i][w] only depends on the previous row dp[i-1]. Using a 1D array and iterating items in the outer loop, with weights in reverse (for 0/1) or forward (unbounded) in the inner loop, achieves the same result with O(W) space. Time remains O(n u00d7 W). For subset sum problems (boolean values), a Python bitset optimization uses bitwise OR: “dp |= dp << num" which processes all capacity values simultaneously using machine-word parallelism, giving O(n u00d7 W/64) effective time."
}
}
]
}

  • Coinbase Interview Guide
  • LinkedIn Interview Guide
  • Atlassian Interview Guide
  • Uber Interview Guide
  • Apple Interview Guide
  • Companies That Ask This

    Scroll to Top