Dynamic Programming
Advanced 12 min read

Knapsack problems

In one line

Knapsack DP tracks the best value for every capacity from 0 to W; looping capacities backwards uses each item once (0/1), looping forwards lets items repeat (unbounded).

What it is

In the 0/1 knapsack problem you have n items, each with a weight and a value, and a bag of capacity W. Choose a subset with total weight at most W and maximum total value. Each item is either taken once or not at all.

The DP state is dp[i][c] = best value using the first i items with capacity c. For item i you either skip it (dp[i-1][c]) or take it if it fits (dp[i-1][c - w] + v). The table has n x W states, so the time is O(n x W). This is pseudo-polynomial: fast when W is small, but W can be exponentially large in the number of bits used to write it, and 0/1 knapsack is NP-hard in general.

In the unbounded knapsack each item can be used any number of times. Coin change (fewest coins to make an amount) and coin change II (number of ways) are unbounded knapsacks. Subset sum, partition equal subset sum and target sum are 0/1 knapsacks with booleans or counts instead of values.

Why interviewers ask

Knapsack is the template behind a large family of interview problems. Interviewers check that you can spot the 'choose items under a budget' shape, write the take-or-skip recurrence, explain why the 1D array is iterated backwards for 0/1 and forwards for unbounded, and explain why greedy by value per weight fails here.

How it works

  1. 1State: dp[c] = best value achievable with capacity c using the items processed so far. Start with dp = [0] * (W + 1).
  2. 20/1: for each item (w, v), for c from W down to w: dp[c] = max(dp[c], dp[c - w] + v). Going down means dp[c - w] still refers to the state before this item, so it is not used twice.
  3. 3Unbounded: for each item, for c from w up to W: same update. Going up means dp[c - w] may already include this item, which is exactly what allows reuse.
  4. 4Counting ways (coin change II): dp[0] = 1 and dp[a] += dp[a - coin], with coins in the outer loop so each combination is counted once regardless of order. Putting amounts in the outer loop counts ordered sequences (permutations) instead.
  5. 5Minimum count (coin change): dp[0] = 0, others infinity, dp[a] = min(dp[a], dp[a - coin] + 1).

Worked example · 0/1 knapsack: weights [1, 3, 4], values [15, 20, 30], capacity 4

  1. Start dp = [0, 0, 0, 0, 0] for capacities 0 to 4.
  2. Item (w 1, v 15), c from 4 down to 1: each dp[c] = max(0, dp[c-1] + 15) = 15. dp = [0, 15, 15, 15, 15].
  3. Item (w 3, v 20), c = 4: max(15, dp[1] + 20 = 35) = 35. c = 3: max(15, dp[0] + 20) = 20. dp = [0, 15, 15, 20, 35].
  4. Item (w 4, v 30), c = 4: max(35, dp[0] + 30 = 30) = 35. dp unchanged.
  5. Answer 35: the items with weights 1 and 3. Greedy by value per weight happens to find this too, but it is not safe: with weights [1, 2, 3], values [6, 10, 12] and capacity 5, greedy takes ratios 6 then 5 (value 16) and the last item no longer fits, while the DP finds weights 2 + 3 for value 22.
  6. If the loop for item 1 had gone forwards, dp[2] would become dp[1] + 15 = 30, using item 1 twice: that is the unbounded version.

Picture it

Packing a carry-on bag for a weekend: for every possible amount of space left you remember the best outfit set that fits. When you consider a new item, you ask, for each amount of space, whether taking it plus the best set for the remaining space beats what you already had.

Reference code

def knapsack_01(weights, values, cap):
    """Each item at most once."""
    dp = [0] * (cap + 1)               # dp[c] = best value with capacity c
    for w, v in zip(weights, values):
        for c in range(cap, w - 1, -1):    # backwards: item used once
            dp[c] = max(dp[c], dp[c - w] + v)
    return dp[cap]


def coin_change(coins, amount):
    """Unbounded: fewest coins to make amount, or -1."""
    INF = float("inf")
    dp = [0] + [INF] * amount
    for coin in coins:
        for a in range(coin, amount + 1):  # forwards: coin reusable
            dp[a] = min(dp[a], dp[a - coin] + 1)
    return dp[amount] if dp[amount] != INF else -1

# knapsack_01([1, 3, 4], [15, 20, 30], 4) -> 35
# coin_change([1, 3, 4], 6) -> 2

The only difference between the two is the loop direction over capacity: backwards keeps each item single-use, forwards allows reuse. Both run in O(n x W) time with O(W) space.

Key patterns

0/1 knapsack (max value)

When: Each item once, maximise value under a capacity

How: 1D dp, capacity loop backwards, dp[c] = max(dp[c], dp[c-w] + v).

Subset sum / partition equal subset sum

When: Can some subset hit an exact target (or half the total)?

How: Boolean dp, dp[0] = True, backwards loop: dp[c] = dp[c] or dp[c - x]. Partition: target = total / 2, fail fast if total is odd.

Coin change (min coins)

When: Unlimited items, fewest to reach an exact amount

How: dp[0] = 0, others infinity, forwards loop, dp[a] = min(dp[a], dp[a-coin] + 1).

Coin change II (count combinations)

When: Number of ways, order does not matter

How: dp[0] = 1, coins outer loop, amounts inner forwards: dp[a] += dp[a - coin].

Target sum (+/- signs)

When: Assign + or - to each number to reach a target

How: Convert to subset sum: positives sum to (total + target) / 2, then count subsets.

Real-world use

Budget and portfolio selection

Capital budgeting (choosing projects under a fixed budget to maximise return) and choosing which features to fit into a release are 0/1 knapsacks; real systems use DP when budgets are small integers, otherwise integer programming solvers.

Cloud resource packing

Choosing which jobs to run on a machine with fixed CPU or memory to maximise priority is knapsack-like; schedulers such as those in Kubernetes use heuristics because the multi-dimensional version is NP-hard.

Making change and payments

Coin change DP gives the fewest coins or notes for currency systems where greedy fails, and the counting version answers how many ways an amount can be paid.

Cryptography history

The Merkle-Hellman cryptosystem was built on the hardness of subset sum (a knapsack); it was broken because its special 'easy' structure leaked, a good example of pseudo-polynomial vs truly hard instances.

In a project

A marketing team has a 20,000 dollar monthly budget and 60 candidate campaigns, each with a cost (in hundreds of dollars) and a predicted number of sign-ups. They want the set of campaigns that maximises sign-ups within the budget.

  1. Scale costs to whole units of 100 dollars so the capacity is 200 and the DP table is small.
  2. Run 0/1 knapsack with a 1D array iterated backwards: 60 items x 201 capacities, about 12,000 updates.
  3. Keep the 2D table, or a choice bit per (item, capacity), to reconstruct which campaigns were chosen.
  4. Compare against a value-per-cost greedy to show the team how much the greedy plan leaves on the table, and rerun for different budgets to plot sign-ups versus spend.

Impact: The team gets the provably best campaign mix for any budget instantly, plus a spend-versus-sign-ups curve that shows where extra budget stops paying off.

Trade-offs

Use it when

  • Choosing a subset of items under one integer capacity or budget
  • Exact-sum, partition and counting-combinations problems
  • The capacity W is small enough that n x W fits in time and memory

Avoid it when

  • Items can be split: fractional knapsack is greedy by value per weight
  • W is huge (like 10^9) with few items: use meet-in-the-middle or DP over value instead
  • Several capacity dimensions at once: state explodes, consider heuristics or an ILP solver

Cost: O(n x W) time, O(W) space with the 1D array (O(n x W) if you need to reconstruct the chosen items). Pseudo-polynomial; the general problem is NP-hard.

Common mistakes

  • Looping capacity forwards in 0/1 knapsack, which silently reuses items.
  • Using greedy by value per weight for 0/1 knapsack.
  • In coin change II, putting amounts in the outer loop and counting permutations instead of combinations.
  • Initialising min-coin dp with 0 instead of infinity, or forgetting to return -1 when unreachable.
  • Calling O(n x W) polynomial; it depends on the numeric value of W.

Interview questions

Quick revision

  • Take or skip: dp[c] = max(dp[c], dp[c - w] + v).
  • 0/1: capacity loop backwards. Unbounded: forwards.
  • Subset sum: boolean dp; partition: target = total / 2.
  • Coin change min: dp[0] = 0, rest infinity.
  • Coin change II: coins outer loop = combinations.
  • O(n x W) time, O(W) space; pseudo-polynomial.
  • Fractional knapsack is greedy; 0/1 is DP.

Check yourself

1. In the 1D 0/1 knapsack, which loop direction over capacity is correct?

2. Weights [1, 3, 4], values [15, 20, 30], capacity 4. What is the maximum value?

3. Why is O(n x W) called pseudo-polynomial?

4. Coins [1, 2], amount 3. Coin change II counts combinations. What is the answer?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free