Search Tech Journey

Find topics, journeys and posts

back to blog
algorithmsintermediate 75m read

L38 · DP VI — Unbounded Knapsack

The knapsack where each item is available in unlimited supply. One character changes from 0/1 — the capacity loop direction — and Coin Change, Rod Cutting, and Combination Sum IV all fall out of it.

🧩DSAPhase 2 · Dynamic programming· Session 038 of 130 75 min

🎯 Own unbounded knapsack — items in unlimited supply — as the exact mirror of 0/1, and see how Coin Change (min and count), Rod Cutting, and Combination Sum IV are all this one shape.

Series: LeetCode — From Basics to Interview-Ready · Session 38 / 65 · Phase 2 · Dynamic programming

Prerequisites

L37 (0/1 knapsack) is mandatory — this session is defined as its opposite. If you cannot yet say "0/1 sweeps capacity backwards", stop and finish L37 first. Everything here is the same table with the inner loop reversed.

Watch first (skim before the session)

Why this session exists

Unbounded knapsack is 0/1's mirror image, and interviewers test whether you can tell them apart under pressure. The naked version — "items with weight and value, capacity W, each item usable unlimited times, maximise value" — is rare; the disguises are everywhere: Coin Change (fewest coins), Coin Change 2 (how many ways), Rod Cutting (best price to cut a rod), Combination Sum IV (count ordered ways to reach a target).

Two details carry this session. First: the single-character difference from 0/1 — the capacity loop runs forwards so dp[c-w] already includes the current item, making it reusable. Second, and subtler: for the counting variants, the order of the two loops decides whether you count combinations (item loop outside) or permutations (capacity loop outside). That loop-order distinction is a favourite senior-level probe.

A vending machine, not a backpack
🌍 Real world
In 0/1 you're a thief with one of each item. In unbounded you're at a vending machine: unlimited stock of each product. If a 25-cent coin helps, you can use as many 25-cent coins as you like. 'Unlimited supply' is the whole difference.
💻 Code world
# Coin Change: coins available in unlimited quantity # amount = 11, coins = [1, 2, 5] # fewest coins = 3 -> 5 + 5 + 1 # each coin reusable -> unbounded

Blank-file warm-up

Five minutes, empty file, no notes. Write both cores from memory, and note the one flipped line:

unbounded value :  for c from w to W:            # FORWARDS (reusable)
                        dp[c] = max(dp[c], dp[c-w] + v)
 
coin change min :  for c from coin to amount:    # FORWARDS
                        dp[c] = min(dp[c], dp[c-coin] + 1)

If the loop direction wobbles against L37, drill the pair: "one-of-each backwards, unlimited forwards."

From first principles

From first principles
Start with the question
Why does forward capacity iteration make items reusable, and how is that different from 0/1?
  1. 1
    Define dp[c] = best value (or fewest coins / count of ways) for exactly capacity/amount c.
    forced by · The budget is the only state that matters once items are unlimited — 'which items I've considered' no longer constrains me, since I can always take more of any item.
  2. 2
    For item (w, v): dp[c] = max(dp[c], dp[c-w] + v), iterating c from w up to W.
    forced by · When we reach c, dp[c-w] has ALREADY been updated for this same item in this pass, so taking the item again is allowed — that's exactly 'reuse'.
  3. 3
    Contrast 0/1: there we iterate c downwards so dp[c-w] reflects the state BEFORE the current item, forbidding reuse.
    forced by · The direction is the sole encoder of 'at most once' vs 'unlimited'. Everything else about the two tables is identical.
  4. 4
    For the take-branch in the recursive framing, unbounded STAYS on the same item index after taking; 0/1 MOVES to the next index.
    forced by · Staying lets you take the same item again; moving on forbids it. This mirrors the loop direction in the iterative version.
⇒ Therefore
Unbounded knapsack = 0/1 with the capacity loop reversed (forwards). The take-branch reuses the current item instead of consuming it.

Unbounded value maximisation (the naked form):

def unbounded_knapsack(weights: list[int], values: list[int], W: int) -> int:
    dp = [0] * (W + 1)
    for c in range(1, W + 1):
        for w, v in zip(weights, values):
            if w <= c:
                dp[c] = max(dp[c], dp[c - w] + v)   # reuse allowed: dp[c-w] may include this item
    return dp[W]
 
assert unbounded_knapsack([1, 3, 4], [15, 50, 60], 8) == 130   # 4+4 -> 60+60

Coin Change — the canonical disguise

Fewest coins to make an amount, coins reusable — unbounded knapsack minimising count.

def coin_change(coins: list[int], amount: int) -> int:
    INF = amount + 1
    dp = [INF] * (amount + 1)
    dp[0] = 0                                 # 0 coins make amount 0
    for c in range(1, amount + 1):
        for coin in coins:
            if coin <= c:
                dp[c] = min(dp[c], dp[c - coin] + 1)   # forwards: reuse coins
    return dp[amount] if dp[amount] != INF else -1
 
assert coin_change([1, 2, 5], 11) == 3       # 5 + 5 + 1
assert coin_change([2], 3) == -1             # impossible
assert coin_change([1], 0) == 0
  • Complexity: O(amount · len(coins)) time, O(amount) space.
  • Follow-up: return the actual coins (parent pointers), or count the number of ways (Coin Change 2), which flips it to a counting problem with a loop-order subtlety.

Coin Change 2 — where loop order bites

Count the number of combinations of coins that make the amount. Coins outer, amount inner:

def coin_change_2(amount: int, coins: list[int]) -> int:
    dp = [0] * (amount + 1)
    dp[0] = 1                                 # one way to make 0: take nothing
    for coin in coins:                        # coins OUTER  -> counts combinations
        for c in range(coin, amount + 1):     # forwards -> reusable
            dp[c] += dp[c - coin]
    return dp[amount]
 
assert coin_change_2(5, [1, 2, 5]) == 4      # {5},{2,2,1},{2,1,1,1},{1x5}
assert coin_change_2(3, [2]) == 0

If you swap the loops (amount outer, coins inner) you count permutations instead — [1,2] and [2,1] become distinct — which is Combination Sum IV, a different problem. This is the senior probe.

Mental model0/1's mirror, with a loop-order twist for counting
Same grid as 0/1 knapsack, but the sweep runs left-to-right so an item can feed itself. For MAX-VALUE the loop order is free. For COUNTING, put the item loop OUTSIDE to count combinations (order-insensitive), or the capacity loop outside to count permutations (order-sensitive).
  • Unlimited supply ⇒ capacity loop FORWARDS.
  • Fewest items to hit a target ⇒ dp[c] = min(dp[c], dp[c-w] + 1).
  • Count COMBINATIONS ⇒ items outer, capacity inner.
  • Count PERMUTATIONS ⇒ capacity outer, items inner (Combination Sum IV).
🔔 Fires when you see
A target/budget with items in unlimited supply — coins, rod lengths, step sizes.

Memory hook

"Unlimited, sweep forwards — coins outside for combos, target outside for orders." The first clause pairs with L37's "one of each, sweep backwards." The second clause encodes the only genuinely tricky bit of this session — the counting loop order. Recite the full pair (L37 + L38) as one sentence and both knapsacks are locked.

Common misconception
✗ What most people think
Coin Change and Coin Change 2 are the same problem with a different return value, so the same loop structure works.
✓ What is actually true
Coin Change (fewest coins) is order-agnostic and loop order is free. Coin Change 2 (count ways) REQUIRES coins-outer to count combinations; capacity-outer counts permutations and gives a larger, wrong answer.
Why the myth is so sticky
For amount 3, coins [1,2]: combinations = {1+1+1, 1+2} = 2. Permutations = {1+1+1, 1+2, 2+1} = 3. Same DP array, different loop nesting, different count.
Prove it to yourself
Before writing a counting knapsack, ask: 'do [1,2] and [2,1] count as one way or two?' One way ⇒ coins outer. Two ways ⇒ capacity outer.

What interviewers actually ask

  • LC322 · Coin ChangeAmazon, Google. Fewest coins for an amount. Probe: recognising unbounded knapsack, the forward loop, and the "impossible ⇒ -1" sentinel. Follow-up: reconstruct the coins, or handle huge amounts.
  • LC518 · Coin Change 2Amazon. Count combinations. Probe: the coins-outer loop order and why it counts combinations not permutations. This is the discriminating question.
  • LC377 · Combination Sum IVGoogle. Despite the name, it counts permutations (ordered), so it's the capacity-outer twin of Coin Change 2. Probe: can you articulate that the loop order flips the meaning?
  • LC279 · Perfect SquaresAmazon. Fewest perfect squares summing to n = Coin Change with coins = {1,4,9,16,...}. Probe: seeing the reduction, and the sqrt bound on the coin set.
  • LC983 · Minimum Cost For TicketsGoogle. An unbounded-flavoured DP over days with 1/7/30-day passes. Probe: state as cost-to-cover-up-to-day-i with reusable passes.
  • LC518 vs LC377 side by side — interviewers sometimes ask both to see if you truly understand loop order versus having memorised one template.

Escalation ladder: fewest items (min) → count combinations → count permutations → reconstruct the multiset. The middle two are the same DP with the loops swapped — the whole point of the session.

Tradeoff

The tradeoff
When a counting-knapsack problem appears, how do you decide the loop nesting?
Items (coins) outer, capacity inner
+ you gain Counts COMBINATIONS — order-insensitive; correct for 'how many sets of coins make X' (Coin Change 2).
− you pay Wrong if the problem actually treats [1,2] and [2,1] as distinct.
pick when when order does NOT matter (a multiset of items).
Capacity (target) outer, items inner
+ you gain Counts PERMUTATIONS — order-sensitive; correct for Combination Sum IV / step-climbing counts.
− you pay Over-counts if the problem wants combinations.
pick when when order DOES matter (sequences, ordered ways to reach a total).
What a senior engineer actually does
First answer the English question: is [1,2] the same as [2,1]? Same ⇒ items outer. Different ⇒ target outer. Decide this BEFORE writing code; it is the single most tested nuance here.

In practice

Making change is the textbook example, but the same DP powers cutting-stock optimisation (cut raw material into pieces to maximise value — Rod Cutting is literal unbounded knapsack) and billing/pass-selection systems that pick the cheapest combination of reusable passes to cover a period. Whenever a resource is available in unlimited units and you're optimising or counting ways to hit a target, unbounded knapsack is the engine.

You are ready to move on when you can
  • Write unbounded knapsack from memory with the capacity loop forwards, and say why forwards = reusable.
  • State the one-character difference from 0/1 knapsack.
  • Write Coin Change (min) and Coin Change 2 (count) and explain the loop order for the counting one.
  • Explain why Combination Sum IV counts permutations while Coin Change 2 counts combinations.
  • Reduce Perfect Squares to Coin Change with square 'coins'.
Check yourself · click to reveal
★ = stretch question

Practice queue

Solve in this order; log each as cold / warm / hint / failed.

  1. LC322 Coin Change — unbounded min, the gateway.
  2. LC518 Coin Change 2 — count combinations (coins outer).
  3. LC377 Combination Sum IV — count permutations (target outer); feel the flip.
  4. LC279 Perfect Squares — Coin Change with square coins.
  5. LC983 Minimum Cost For Tickets — unbounded-flavoured day DP.
Key points