HARD Lab · Dynamic Programming · 27

322. Coin Change

Given coin denominations and a target amount, find the fewest coins that sum to it — or -1if it's impossible. A bottom-up DP table over amounts fills each cell with the minimum over every usable coin, exploiting the fact that each denomination can be reused without limit.

MediumDPUnbounded KnapsackTypeScript

PROBLEM What we're solving

Given coins [1, 2, 5] and amount = 11, return the fewest coins summing to 11. Answer: 3 (two 5s and one 1: 5 + 5 + 1). Each denomination can be used any number of times. If no combination reaches the target, return -1.

KEY IDEA Build answers bottom-up: each amount depends on smaller amounts

Insight → dp[a] = the minimum coins to make amount a. For every coin c ≤ a, we already know dp[a−c] (a strictly smaller sub-problem), so the best we can do using coin c is dp[a−c] + 1. Take the min over all coins. This is the unbounded-knapsack recurrence: coins can repeat because we re-read the same dparray we're filling.

RECURRENCE The DP formula and iteration order

  • 0 · Base case. dp[0] = 0 — zero coins needed to reach amount zero. All other cells start at Infinity (unreachable sentinel).
  • 1 · Outer loop: amounts 1 → amount. Fill left to right so that sub-problems are always ready before we need them.
  • 2 · Inner loop: every coin c. If c ≤ a and dp[a−c] ≠ Infinity, then dp[a] = min(dp[a], dp[a−c] + 1). Skipping Infinity sources prevents a silent integer-overflow bug.
  • 3 · Answer. dp[amount]. Still Infinity? Return -1; no combination reaches the target.
Classic confusion → mixing up the loop order between the 0/1 knapsack (outer: items; inner: capacity backwards) and the unbounded knapsack / coin change (outer: amounts forward; inner: coins). Here the outer loop is over amounts and runs forward because reuse is allowed — reading dp[a−c] for the same pass is exactly what lets a coin be used multiple times.

COST Complexity & alternatives

Brute-force DFS
O(k^(amount/min))
Exponential — retries the same sub-amounts.
Bottom-up DP
O(amount × k)
O(amount) space; O(amount × k) time.

k = number of coin denominations. Space is O(amount) for the 1-D table — far less than the O(amount × k) 2-D table a 0/1-knapsack would need. Top-down memoized DFS reaches the same asymptotic cost but incurs call-stack overhead.

Pattern transfer →the same recurrence (1-D table, forward loop, “+1 per item used”) powers Perfect Squares (LC 279), Combination Sum IV (LC 377, counting paths instead of minimizing), and Minimum Cost for Climbing Stairs. Any time the prompt says “minimum number of [unlimited-supply items] to reach a target” — reach for this template.

RUN IT Fill dp[0..amount] bottom-up, take the minimum

step 0 / 62
STARTInitialize dp[0..11]: dp[0] = 0, all others . We want the minimum coins to reach each amount.
dp[a]001234567891011
State
a
coin
candidate
dp[a]
current dp[a] being filledsource dp[a−coin] consultedfinal answer
slowfast

TYPESCRIPT The solution, annotated

coinChange.ts
function coinChange(coins: number[], amount: number): number {
  const dp: number[] = new Array(amount + 1).fill(Infinity);
  dp[0] = 0; // base case: 0 coins needed to reach 0

  for (let a = 1; a <= amount; a++) {
    for (const c of coins) {
      if (c <= a && dp[a - c] !== Infinity) {
        dp[a] = Math.min(dp[a], dp[a - c] + 1);
      }
    }
  }

  return dp[amount] === Infinity ? -1 : dp[amount];
}

Reading it block by block

Line 2–3 — allocate and seed. A 1-D array of length amount + 1 filled with Infinityrepresents “not yet reachable.” Setting dp[0] = 0 is the only hard-coded base case; every other answer is derived from it.
Lines 5–10 — fill bottom-up. The outer loop marches a from 1 to amount (left to right) guaranteeing that when we compute dp[a], every dp[a−c] is already final. The inner loop tries every coin; the guard c <= a avoids negative indices and the !== Infinity guard prevents a silent wrap-around.
Line 9 — the recurrence. dp[a - c] + 1: the minimum coins to reach a - c, plus the one coin c we just decided to use. Math.minkeeps the best option found so far across all coins. Coins are unbounded because we read from the same array being filled — no need for a separate “used” dimension.
Line 13 — the -1 sentinel. If dp[amount] is still Infinity, no combination of the given coins sums to amount. The ternary converts the sentinel to the problem's required -1.
Complexity → O(amount × k) time — every cell dp[a] loops over all k coins once. O(amount) space for the 1-D table. No recursion overhead; the iterative approach is both faster in practice and easier to bound.

INTERVIEWFollow-ups they'll ask

  • “Return the actual coins used, not just the count.” Store a parent[a] array recording which coin was chosen at each amount, then walk back from amount to 0.
  • “Count the number of ways instead of the minimum.” Change Math.min(dp[a], dp[a-c]+1) to dp[a] += dp[a-c] and seed dp[0] = 1— that's Coin Change II (LC 518).
  • “What if each coin can only be used once?”That's the classic 0/1 knapsack: run the inner loop backwardsover amounts so earlier choices aren't re-used in the same pass.
  • “Can you do it top-down?” Yes — memoized DFS with a Map<number, number>; same O(amount × k) time but with O(amount) call-stack depth.
  • “Edge cases?” amount = 0 returns 0 immediately (base case). coins = [2], amount = 3 returns -1 (odd target, only even denomination).

MNEMONIC The one-liner

"Start from zero, march to the target — each amount asks every coin for the cheapest path."

TRIGGERS When you see ___ → reach for ___

"minimum number of coins/items to reach target"1-D DP, forward loop, dp[a] = min(dp[a], dp[a-c]+1)
denominations can be reused unlimited timesunbounded knapsack — read dp[a-c] from the current (same) pass
"if impossible return -1"initialize dp with Infinity; check at the end
"count ways instead of min coins"swap min for sum (dp[a] += dp[a-c]), seed dp[0]=1

SKELETON The reusable shape

skeleton.ts
const dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;

for (let a = 1; a <= amount; a++) {
  for (const c of coins) {
    if (c <= a && dp[a - c] !== Infinity) {
      dp[a] = Math.min(dp[a], dp[a - c] + 1);
    }
  }
}

return dp[amount] === Infinity ? -1 : dp[amount];

FLASHCARDS Tap to flip

What does dp[a] represent?
The minimum number of coins needed to make exactly amount a (Infinity = unreachable).
tap to flip
1 / 8
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
For coins = [1, 2, 5], amount = 11, what does dp[6] equal after the full table is built?
QUESTION 02
What is the correct initialization for the dp array?
QUESTION 03
What is the time complexity of the bottom-up solution?
QUESTION 04
coins = [2], amount = 3. What is returned?
QUESTION 05
Why is the outer loop over amounts (not coins) in the unbounded variant?
QUESTION 06
What single change converts Coin Change (min coins) into Coin Change II (count ways)?
QUESTION 07
After initialization, dp[0] equals: