518. Coin Change II

Given coin denominations and a target amount, count the number of distinct combinations that sum to it. The key trick is an outer loop over coins and an inner loop over amounts — this naturally prevents double-counting permutations of the same combination.

MediumDPUnbounded KnapsackCounting CombinationsTypeScript

PROBLEM What we're solving

Given coins = [1, 2, 5] and amount = 5, return the number of distinct coin combinations that sum to 5. Answer: 4 — those combinations are 5, 2+2+1, 2+1+1+1, and 1+1+1+1+1. Note that 2+1+1+1 and 1+2+1+1 count as the same combination — order does not matter.

KEY IDEA Coins outer, amounts inner — order controls combinations vs permutations

Insight → dp[a] = number of ways to make amount a. The recurrence is dp[a] += dp[a - coin] with dp[0] = 1. The crucial detail is loop order: put coins in the outer loop and amounts in the inner loop. This means each coin is fully committed before moving on, so every counted path uses coins in a fixed order — combinations only, no permutations.

RECIPE Coin outer, amount inner, accumulate ways

  • 0 · Base case. dp[0] = 1 — there is exactly one way to make amount zero: use no coins at all. Every other cell starts at 0.
  • 1 · Outer loop: each coin.We process one denomination at a time. This commits the coin to a fixed “slot” in the combination, preventing the same multiset from being counted under different orderings.
  • 2 · Inner loop: amounts from coin → amount (forward). For each a, add dp[a - coin] — the ways already counted that can be extended by one more of this coin. Starting at a = coin avoids negative indexing.
  • 3 · Answer. dp[amount] is the total number of combinations. It is always ≥ 0; the problem guarantees an answer exists (0 is valid when no combination reaches the target).
Classic confusion → swapping the loop order — putting amounts outer and coins inner — counts permutations, not combinations. With coins = [1, 2], amount = 3, the coins-outer order gives 2 (combinations: 1+2, 1+1+1) while amounts-outer gives 3 (also counting 2+1 as distinct from 1+2).

COST Complexity & alternatives

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

k = number of coin denominations. The 1-D rolling table uses O(amount) space — the same as Coin Change I. Top-down memoized DFS reaches the same asymptotic cost but requires careful handling of the combination-vs-permutation distinction via the starting-coin index.

Pattern transfer →the “coins outer, amounts inner” template appears in Coin Change I (LC 322, just swap sum for min), Combination Sum IV (LC 377 — counts permutations, so it uses amounts outer), and any “count ways to reach a target with unlimited items” prompt. Recognizing the combination-vs-permutation axis is the key interview signal.

RUN IT Coins outer, amounts inner — count combinations

step 0 / 24
STARTInitialize dp[0..5] to 0; set dp[0] = 1 (one way to make 0: use no coins).
1function change(amount: number, coins: number[]): number {
2 const dp: number[] = new Array(amount + 1).fill(0);
3 dp[0] = 1; // one way to make amount 0: use no coins
4
5 for (const coin of coins) { // outer: iterate coins
6 for (let a = coin; a <= amount; a++) { // inner: amounts forward
7 dp[a] += dp[a - coin]; // add ways that include this coin
8 }
9 }
10
11 return dp[amount];
12}
dp[a]100102030405
State
coin
a
dp[a-coin]
dp[a]
current coin being processeddp[a] being updateddp[a-coin] source valuefinal answer
slowfast

TYPESCRIPT The solution, annotated

coinChangeII.ts
function change(amount: number, coins: number[]): number {
  const dp: number[] = new Array(amount + 1).fill(0);
  dp[0] = 1; // one way to make amount 0: use no coins

  for (const coin of coins) {         // outer: iterate coins
    for (let a = coin; a <= amount; a++) { // inner: amounts forward
      dp[a] += dp[a - coin];          // add ways that include this coin
    }
  }

  return dp[amount];
}

Reading it block by block

Line 2–3 — allocate and seed. A 1-D array of length amount + 1 filled with 0represents “no ways found yet.” dp[0] = 1 is the only hard-coded base case: there is exactly one way to form amount zero — use nothing.
Lines 5–8 — outer loop over coins. Each denomination is processed in full before the next one begins. This is what locks the counting to combinations: once we move past coin c, it can only appear as an earlier “layer”, so no permutation of the same multiset is ever counted twice.
Line 6 — inner loop: amounts forward from coin. Starting at a = coin (not 0) avoids negative indices. Running forward is what makes coins unbounded — when we compute dp[a], dp[a - coin] has already been updated in this same coin pass, so the same denomination can contribute multiple times.
Line 7 — accumulate. dp[a] += dp[a - coin]reads: “every way to build amount a - coin becomes a new way to build aby appending one more of this coin.” The sum across all coins at all amounts gives the total combination count.
Line 11 — return. dp[amount]is the final answer. Unlike Coin Change I, there is no “-1 sentinel” — if no combination reaches the target, the cell simply stays 0.
Complexity → O(amount × k) time — each of the k coins iterates through all amount cells once. O(amount) space for the 1-D table. No recursion overhead.

INTERVIEWFollow-ups they'll ask

  • “Why coins outer instead of amounts outer?” Amounts-outer counts permutations (distinct orderings). Coins-outer ensures each denomination is committed in one sweep, so only combinations (unordered multisets) are counted.
  • “What if order matters (permutations)?” That's Combination Sum IV (LC 377): flip to amounts outer, coins inner — dp[a] += dp[a - coin] for every coin at every amount.
  • “What if each coin may only be used once?”That's the 0/1 knapsack variant: keep coins outer but run the inner loop backwards (a from amount down to coin) so each denomination is only “spent” once.
  • “Can you do it top-down?” Yes — memoized DFS with a coinIndex parameter to fix the coin order and avoid permutation double-counting. Same O(amount × k) time but with call-stack overhead.
  • “Edge cases?” amount = 0 returns 1 (the empty combination). A single coin equal to amount gives 1. No coin divides amount gives 0.

OPTIMAL DP

coinChangeII.ts
function change(amount: number, coins: number[]): number {
  const dp: number[] = new Array(amount + 1).fill(0);
  dp[0] = 1; // one way to make amount 0: use no coins

  for (const coin of coins) {         // outer: iterate coins
    for (let a = coin; a <= amount; a++) { // inner: amounts forward
      dp[a] += dp[a - coin];          // add ways that include this coin
    }
  }

  return dp[amount];
}
Complexity → O(amount × k) time — each of the k coins iterates through all amount cells once. O(amount) space for the 1-D table. No recursion overhead.

ALT 1 Brute force — recurse over (coin index, remaining)

O(2^(amount)) time · O(amount) space

Explore the decision tree directly: at each coin you may either skip it (move to the next coin) or take it (stay on the same coin, subtracting its value). Sum the leaf counts where remaining === 0. Fixing the coin order avoids counting a combination more than once.

approach-2.ts
function change(amount: number, coins: number[]): number {
  function count(i: number, remaining: number): number {
    if (remaining === 0) return 1;      // exact combination found
    if (remaining < 0 || i === coins.length) return 0;

    // Skip coin i, OR take coin i (and may take it again).
    return count(i + 1, remaining)
         + count(i, remaining - coins[i]);
  }

  return count(0, amount);
}
Note → With no memoization the same (i, remaining) states are recomputed across countless branches, blowing up exponentially. Cache results by (i, remaining)— or collapse to the 1-D rolling array above — for the O(amount · coins) optimal.

MNEMONIC The one-liner

"Coins first, amounts second — each coin layer adds its own ways before the next coin arrives."

TRIGGERS When you see ___ → reach for ___

"count the number of combinations/ways to reach a target"coins outer, amounts forward, dp[a] += dp[a-coin], dp[0]=1
denominations unlimited, order does NOT mattercoins-outer DP — fixes ordering, prevents permutation double-count
"count distinct orderings / sequences"amounts outer, coins inner (Combination Sum IV pattern)
counting ways vs minimizing countswap Math.min(…+1) → +=, swap Infinity seed → 0, swap dp[0]=0 → dp[0]=1

SKELETON The reusable shape

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

for (const coin of coins) {
  for (let a = coin; a <= amount; a++) {
    dp[a] += dp[a - coin];
  }
}

return dp[amount];

FLASHCARDS Tap to flip

What does dp[a] represent in Coin Change II?
The number of distinct combinations of coins that sum to exactly amount a.
tap to flip
1 / 8
Answer to reveal explanations. No penalty for retries.Score: 0 / 9
QUESTION 01
For coins = [1, 2, 5], amount = 5, what is the answer?
QUESTION 02
What is the correct initialization for the dp array?
QUESTION 03
Why is the coins loop placed outside the amounts loop?
QUESTION 04
coins = [2], amount = 3. What does the function return?
QUESTION 05
What is the time complexity of the bottom-up solution?
QUESTION 06
What single change converts Coin Change II (count combinations) into Combination Sum IV (count ordered sequences)?
QUESTION 07
After processing only coin = 1 (the first coin) with coins = [1, 2, 5], amount = 5, what is dp[3]?
QUESTION 08
#518 · Coin Change IICount the number of combinations (not permutations) that sum to the target: outer loop over coins, inner loop over amounts. The coin-outer order ensures each denomination is counted any number of times without double-counting orderings.Which algorithmic approach does this primarily use?
QUESTION 09
#518 · Coin Change IICount the number of combinations (not permutations) that sum to the target: outer loop over coins, inner loop over amounts. The coin-outer order ensures each denomination is counted any number of times without double-counting orderings.Which implementation correctly solves it?