Coin Change
The Coin Change family covers two distinct dynamic-programming problems on a multiset of coin denominations and a target amount: (a) the minimum-coins variant — find the fewest coins (with unlimited supply per denomination) that sum to the target, returning
-1if impossible (LeetCode 322); and (b) the count-the-ways variant — count the number of distinct combinations of coins that sum to the target (LeetCode 518). Both are unbounded-knapsack-flavored DPs over a 1D statedp[amount], but the iteration order differs sharply between them, and the count variant has a famous trap that double-counts permutations as distinct combinations if the loops are nested in the wrong order. This note treats both side by side and contrasts them with the greedy approach (which fails in general, despite working on US currency).
1. Intuition — The Cashier’s Drawer
You are the cashier in a shop. A customer is owed $0.41 in change, and your drawer holds an unlimited supply of pennies (1¢), nickels (5¢), dimes (10¢), and quarters (25¢). Two natural questions arise:
- (a) Minimum coins. What is the smallest number of coins that sums to 41¢? Most cashiers’ instinct is greedy: take the biggest coin that fits, repeat. With US denominations: 25 + 10 + 5 + 1 = 41 → 4 coins. That happens to be optimal here. But this is a coincidence of US coinage; we will see in §10.1 that for arbitrary denominations the greedy approach can produce suboptimal answers, and that is precisely why we need DP.
- (b) Number of ways. In how many distinct combinations can you assemble 41¢? Some examples: 41 pennies; 1 nickel + 36 pennies; 1 dime + 31 pennies; 1 quarter + 16 pennies; 1 quarter + 1 nickel + 11 pennies; etc. We do not care about the order in which the coins are stacked (using a quarter then a dime is the same combination as a dime then a quarter); we only care about the multiset of coins used.
These two questions have different aggregations (a min vs a sum), different base cases (∞ vs 1), and — crucially — different iteration orders in their tabulation. They are siblings, not the same problem, and the differences are pedagogically important.
The change-making problem in its general form (with arbitrary denominations) was studied by J. W. Wright in 1975 (“The change-making problem,” Journal of the ACM) and shown to be NP-hard in the size of the binary encoding of the amount — but the standard O(amount · |coins|) DP is pseudo-polynomial and runs comfortably for interview-sized inputs. This is the same pseudo-polynomial nature as 01 Knapsack and Subset Sum.
2. Tiny Worked Example
Let coins = [1, 2, 5] and amount = 11 for both variants.
2.1 Variant A — Minimum Coins
State: dp[a] = minimum number of coins summing to amount a. Recurrence: dp[a] = 1 + min over c in coins, c ≤ a of dp[a - c]. Base: dp[0] = 0 (zero coins make zero); dp[a] = +∞ for unreached amounts.
We iterate a = 0, 1, 2, ..., 11 and for each a try every coin c ≤ a. The table fills as follows (∞ shown as - for readability):
| a | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| init | 0 | - | - | - | - | - | - | - | - | - | - | - |
| a=1 | 0 | 1 | - | - | - | - | - | - | - | - | - | - |
| a=2 | 0 | 1 | 1 | - | - | - | - | - | - | - | - | - |
| a=3 | 0 | 1 | 1 | 2 | - | - | - | - | - | - | - | - |
| a=4 | 0 | 1 | 1 | 2 | 2 | - | - | - | - | - | - | - |
| a=5 | 0 | 1 | 1 | 2 | 2 | 1 | - | - | - | - | - | - |
| a=6 | 0 | 1 | 1 | 2 | 2 | 1 | 2 | - | - | - | - | - |
| a=7 | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | - | - | - | - |
| a=8 | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | - | - | - |
| a=9 | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 3 | - | - |
| a=10 | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 3 | 2 | - |
| a=11 | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 3 | 2 | 3 |
Sample derivations: dp[3] = min(dp[3-1]+1, dp[3-2]+1) = min(1+1, 1+1) = 2. dp[5] = min(dp[4]+1, dp[3]+1, dp[0]+1) = min(3, 3, 1) = 1. dp[11] = min(dp[10]+1, dp[9]+1, dp[6]+1) = min(3, 4, 3) = 3 — achievable via 5+5+1 or 5+2+2+2 (the latter uses 4 coins, not 3, so the minimum is from 5+5+1).
Answer: dp[11] = 3.
2.2 Variant B — Number of Ways (Combinations)
State: dp[a] = number of distinct combinations summing to a. Recurrence (correct version): loop over coins outermost, for each coin c loop a = c..amount and add dp[a] += dp[a - c]. Base: dp[0] = 1 (one way to make 0: the empty multiset), dp[a > 0] = 0 initially.
We iterate coins in the order 1, 2, 5. After processing coin 1, the table represents “number of ways using only coin 1” — one way per amount (use that many pennies):
| a | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| init | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| after coin 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| after coin 2 | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | 6 | 6 |
| after coin 5 | 1 | 1 | 2 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 |
After processing coin 2 (running a = 2..11 and adding dp[a-2] to dp[a]), we now count combinations using coins from {1, 2}. For instance, dp[4] = 3: those are 1+1+1+1, 1+1+2, 2+2. After processing coin 5, the final row counts combinations using {1, 2, 5}.
Answer: dp[11] = 11. (Spot-check: combinations include 1×11, 1×9 + 2×1, 1×7 + 2×2, 1×5 + 2×3, 1×3 + 2×4, 1×1 + 2×5, 5 + 1×6, 5 + 1×4 + 2×1, 5 + 1×2 + 2×2, 5 + 2×3, 5 + 5 + 1 — that’s 11.)
2.3 The Wrong Iteration Order — Permutations Instead of Combinations
If you instead loop a outermost and coins innermost (mirroring variant A’s loop structure), you count permutations: 1+2 and 2+1 become distinct, vastly inflating the count. We will return to this in the pitfalls (§11.2).
3. Pseudocode
Variant A — Minimum coins:
dp[0] = 0
for a in 1..amount:
dp[a] = +∞
for each coin c in coins:
if c <= a and dp[a - c] + 1 < dp[a]:
dp[a] = dp[a - c] + 1
return dp[amount] if dp[amount] < +∞ else -1
Variant B — Number of combinations:
dp[0] = 1
for a in 1..amount:
dp[a] = 0
for each coin c in coins: # OUTER loop is coins
for a in c..amount: # INNER loop is amount, ASCENDING
dp[a] += dp[a - c]
return dp[amount]
The pseudocode mirrors the table fills from §2. Note the critical asymmetry: A puts amount outside, coins inside; B puts coins outside, amount inside. A’s order does not double-count because the min aggregation is order-insensitive; B’s order is the entire mechanism that prevents double-counting.
4. Python Implementation
4.1 Variant A — Minimum Coins
Top-down (memoization):
from functools import lru_cache
def coinChange_topdown(coins: list[int], amount: int) -> int:
@lru_cache(maxsize=None)
def dp(a: int) -> int:
if a == 0:
return 0
if a < 0:
return float('inf')
return min(dp(a - c) for c in coins) + 1
result = dp(amount)
return result if result < float('inf') else -1The function recurses over remaining amount a. The base case a == 0 returns 0 (zero coins needed); a < 0 is an unreachable / over-shoot signal that returns +∞ so that the enclosing min ignores that branch. The +1 accounts for the coin we just “spent.” The @lru_cache decorator (Python functools standard library) memoizes on a, which has at most amount + 1 distinct values — bounding the work to O(amount · |coins|).
Bottom-up (tabulation):
def coinChange_bottomup(coins: list[int], amount: int) -> int:
INF = amount + 1 # sentinel larger than any valid answer
dp = [INF] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return dp[amount] if dp[amount] <= amount else -1We use the sentinel INF = amount + 1 rather than float('inf') because any valid answer is at most amount (worst case is amount 1-cent coins), so this sentinel is comfortably larger than any reachable value while staying as a Python int (faster comparisons than floats). The final check dp[amount] <= amount distinguishes “computed a real value” from “still at sentinel.” Time is O(amount · |coins|), space is O(amount).
4.2 Variant B — Number of Combinations
Top-down (memoization), with explicit “next-coin” index to avoid double-counting:
from functools import lru_cache
def coinChange2_topdown(amount: int, coins: list[int]) -> int:
coins = sorted(coins) # canonical order for combinations
@lru_cache(maxsize=None)
def dp(remaining: int, i: int) -> int:
# Number of combinations using coins[i:] summing to `remaining`.
if remaining == 0:
return 1
if i == len(coins) or remaining < 0:
return 0
# Choice: use coin i (and stay on coin i, allowing re-use), or skip to coin i+1.
return dp(remaining - coins[i], i) + dp(remaining, i + 1)
return dp(amount, 0)Here the second argument i enforces a canonical order on combinations: we either keep using the current coin coins[i] (unbounded re-use) or advance to coins[i+1], but we never go back. This eliminates the 1 + 2 vs 2 + 1 duplication at the source — the recursion only ever generates combinations in non-decreasing coin-index order. State space is O(amount · |coins|); per-state work is O(1).
Bottom-up (tabulation) — the canonical form:
def coinChange2_bottomup(amount: int, coins: list[int]) -> int:
dp = [0] * (amount + 1)
dp[0] = 1
for c in coins: # OUTER: each coin processed once
for a in range(c, amount + 1): # INNER: amounts in ASCENDING order
dp[a] += dp[a - c]
return dp[amount]The loop ordering is the entire correctness argument: by fixing a coin c and sweeping all amounts before moving to the next coin, we ensure each combination (multiset of coins) is generated exactly once. After processing the first k coins, dp[a] equals the number of combinations of coins[0..k-1] summing to a. Re-entering the outer loop with the next coin extends each existing combination by zero or more copies of the new coin. Time O(amount · |coins|), space O(amount).
5. Complexity
For both variants, with n = |coins| and W = amount:
- Time:
O(n · W). Derivation: the state space isO(W)(one cell per amount in 1D), and per state we doO(n)work (in variant A, take aminoverncoins; in variant B, the outer loop multiplies the inner sweep). Equivalently from the “states × work” formula in DP State Identification. - Space:
O(W)for the 1D table. Constant-factor memory savings are possible (e.g., if all coin denominations are small, you can use a 2Ddp[i][a]withO(n · W)space and recover combinations more easily, but the 1D form is what is expected). - Pseudo-polynomial caveat. Same as 01 Knapsack:
O(n · W)is polynomial in the value ofW, not in the number of bits to encodeW. IfW = 10^18, this DP is infeasible regardless of how smallnis. Practical interview values ofW ≤ 10^4(LeetCode 322’s stated bound) make this irrelevant, but the formal complexity classification is weakly NP-hard — established via reduction from subset-sum, with Lueker (1975) and Wright (1975) as the foundational references (the Wikipedia Change-making problem entry traces the chain). - Beyond the textbook DP. Chan and He (SOSA 2020, paper) gave a deterministic
O(W · log W · log log W)algorithm and a randomizedO(W · log W)for the single-target variant — sub-quadratic inWwhennis large — by formulating the recurrence as a sequence-convolution problem. This is well past the interview bar but worth knowing exists: theO(n · W)DP is not the asymptotic ceiling.
6. Variant A — Minimum Coins, Path Reconstruction
If you need not just the count but the actual coins used, store a parent[a] array recording which coin was last used to reach a:
def coinChange_with_path(coins: list[int], amount: int) -> tuple[int, list[int]]:
INF = amount + 1
dp = [INF] * (amount + 1)
parent = [-1] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a and dp[a - c] + 1 < dp[a]:
dp[a] = dp[a - c] + 1
parent[a] = c
if dp[amount] > amount:
return -1, []
# Reconstruct.
path = []
a = amount
while a > 0:
path.append(parent[a])
a -= parent[a]
return dp[amount], pathparent[a] stores the coin used in the optimal transition to a. We backtrack from a = amount until a = 0. This is the general witness-reconstruction template for 1D DPs.
7. Variant B — Permutations Instead of Combinations
If the problem actually asks for permutations (1+2 and 2+1 count as distinct sequences — sometimes called “staircase” or “compositions”), swap the loop order:
def numberOfPermutations(amount: int, coins: list[int]) -> int:
dp = [0] * (amount + 1)
dp[0] = 1
for a in range(1, amount + 1): # OUTER: amount
for c in coins: # INNER: coins
if c <= a:
dp[a] += dp[a - c]
return dp[amount]This is LeetCode 377 (“Combination Sum IV” — confusingly named: despite “combination” in the title, it counts permutations). The loop swap is the entire algorithmic difference. Both are correct DPs; they answer different questions.
8. Equivalence to Unbounded Knapsack
Coin Change variant A is unbounded knapsack with all values equal to 1, minimizing items: each “item” (coin) has weight c and value 1, the knapsack capacity is amount, and we want the minimum total value such that total weight equals amount exactly (rather than the maximum value subject to weight ≤ capacity). Variant B is counting subsets-with-repetition that sum to a target — a sibling of Partition Equal Subset Sum but with repetition allowed.
This unification is useful: any technique you learn for Unbounded Knapsack (rolling-array space, item-by-item processing for combinations) transfers directly. See also the Subset Sum family for the bounded variants.
9. Diagram — How the Two Loop Orders Generate Different Sets
flowchart TD subgraph CombLoop[Outer coins, inner amount — combinations] C1[Process coin 1<br/>extends every existing<br/>combination by some 1s] --> C2[Process coin 2<br/>extends each combination<br/>from C1 by some 2s] --> C3[Process coin 5<br/>extends each combination<br/>from C2 by some 5s] end subgraph PermLoop[Outer amount, inner coins — permutations] P1[At amount a:<br/>add dp from a−1 via coin 1] --> P2[Then add dp from a−2 via coin 2] --> P3[Then add dp from a−5 via coin 5] P3 --> P4[Each amount visited once,<br/>but coin orderings produce<br/>distinct sequences] end
What this diagram shows. The two loop orders implement two different generation processes. The combinations order (top) processes one coin at a time, treating each coin as adding a layer on top of the table built from previous coins; a combination is uniquely associated with the sequence of coins as iterated, so no duplicates arise. The permutations order (bottom) at each amount a considers every coin as a possible “last” coin, which means a sequence ending in 1 then a sequence ending in 2 for the same multiset both get counted. The arrows in the top loop show that combinations are built incrementally over coins; the arrows in the bottom show that permutations are built incrementally over amounts. This visual is the single best mnemonic for which loop goes outside.
10. Greedy Approach — Why It Fails in General
10.1 The US-Coinage Coincidence
For US denominations [1, 5, 10, 25] (and many real-world currencies), the greedy “take the largest coin that fits” approach is optimal. This is not a general property of the change-making problem; it is a specific structural property of these denomination sets. Pearson’s 2005 paper (“A polynomial-time algorithm for the change-making problem,” Operations Research Letters) gives an O(n³) algorithm to test whether a denomination set is “canonical” (i.e., greedy-optimal). For arbitrary denominations, greedy can be arbitrarily bad.
10.2 The Classic Counterexample
Coins [1, 3, 4], target 6. Greedy takes 4 + 1 + 1 = 6 (3 coins). Optimal: 3 + 3 = 6 (2 coins). Greedy committed to the largest coin (4) and was then stuck taking two 1s; it could not “reconsider” its choice. DP enumerates both possibilities and picks correctly.
It is worth emphasizing that no simple closed-form characterization of canonical denomination sets is known. Pearson (2005) gave an O(n³) test by reducing the search for a minimum counterexample to O(n²) candidate amounts — a polynomial-time decision procedure, not a structural theorem (per the published paper). Kozen and Zaks (1994) had earlier given an algorithm polynomial in the largest denomination but not in the input size, leaving the genuinely-polynomial question open until Pearson resolved it. Structural classifications are known only for small fixed-size systems: 2-coin and 3-coin systems have explicit characterizations, 4-coin and 5-coin systems have been classified by case analysis, and 6-coin canonical systems were characterized only as recently as Cai et al. (arXiv:2111.12392). For arbitrary n, the rule remains “run Pearson’s test, do not guess.”
10.3 Lesson
Greedy is O(amount / smallest_coin) and very fast when correct. DP is O(amount · |coins|) and always correct. In an interview, always default to DP for coin-change problems unless the problem explicitly fixes the denomination set to a known-canonical one. Mentioning that greedy works on US coinage but not arbitrary coinage is a good way to demonstrate awareness of the trade-off. For the deeper theory of why greedy works on certain structured inputs — exchange arguments, the matroid framework, and the “stays ahead” technique — see Greedy Algorithms — Proof Techniques; the change-making problem is the canonical motivating example where the natural greedy strategy is not provably optimal, which is precisely how that note frames the bound.
11. Pitfalls
11.1 Returning 0 Instead of -1 for Unreachable Amount (Variant A)
If dp[amount] is still +∞ after the table fills, the amount is unreachable with the given coins — return -1 (LeetCode’s convention). A common bug returns 0 because the sentinel was forgotten. Test on coins = [2], amount = 3 → must return -1.
11.2 Wrong Loop Order for Combinations (Variant B)
This is the single most-tested DP pitfall in interviews. The combinations variant requires coins-outer / amount-inner. Reversed, it counts permutations and inflates the answer. Memorize the rule: for combinations, the coin loop is outer. If you can articulate why (each combination is generated once because we never “go back” to a previous coin), you’ve internalized the recurrence.
11.3 Wrong Base Case dp[0] (Variant B)
dp[0] = 1 means “there is one way to make 0: the empty multiset.” Setting dp[0] = 0 causes the entire table to be 0 (every recurrence ultimately bottoms out at dp[0], and 0 propagates). This is a textbook off-by-one base-case error and is silent — the code runs and returns 0 for everything.
11.4 Iterating a from 0 Instead of c in the Inner Loop (Variant B)
The inner loop must start at a = c, because for a < c you cannot use coin c. Iterating from a = 0 and writing if c <= a: dp[a] += dp[a - c] is correct but slightly wasteful; the cleaner form is for a in range(c, amount + 1). This is a style nit, not a correctness issue, but interview reviewers notice.
11.5 Using float('inf') as Sentinel (Variant A)
float('inf') works but introduces floating-point comparisons in a hot loop, slower than integer comparisons. The idiom INF = amount + 1 exploits the fact that any valid answer is at most amount (use that many 1-cent coins) — so amount + 1 is a strict upper bound on any reachable dp[a]. After the fill, dp[amount] > amount cleanly detects unreachability.
11.6 Confusing “Amount” with “Number of Coins”
dp[a] indexes by amount (an integer in cents/dollars), not by coin count. Easy slip when typing dp[len(coins)] or dp[amount + len(coins)]. The state is amount, period.
11.7 Top-Down with Mutable List in Cache Key
If you parametrize the recursion by a list of remaining coins (e.g., for a “use each coin at most once” twist), lru_cache will fail because lists are unhashable. Convert to tuple, or — better — parametrize by an index into the sorted coin list rather than carrying the list itself. The index is a single integer, and the cache key shrinks accordingly.
11.8 Greedy Trap
If the interviewer presents the problem with US-like denominations, your gut may say “greedy works.” Fight the gut. Even if greedy passes the sample test cases, it is wrong for arbitrary denominations and the interviewer may add a hidden test case ([1, 3, 4], target 6) to expose it. Always write the DP.
11.9 Coins with Value 0
Some pathological inputs include coin = 0. Variant A would loop forever (dp[a] = 1 + dp[a - 0] = 1 + dp[a]); variant B would infinite-loop similarly. Sanitize input: filter out non-positive coins or assert on entry.
11.10 Negative Amount Handling
Recursive variant A might be called with negative a if you forget the c <= a guard. The base case a < 0 → +∞ (in the top-down version) catches this; the bottom-up form prevents it via the if c <= a check.
12. Diagram — The Decision Tree for Variant A on coins=[1,2,5], amount=5
flowchart TD A5[dp 5 = ?] --> S1[via coin 1: dp 4 + 1] A5 --> S2[via coin 2: dp 3 + 1] A5 --> S5[via coin 5: dp 0 + 1 = 1] S1 --> A4[dp 4 = 2] S2 --> A3[dp 3 = 2] S5 --> A0[dp 0 = 0 base] A5 --> R[dp 5 = min 3, 3, 1 = 1]
What this diagram shows. The recurrence at amount 5 evaluates three transitions — one per coin denomination ≤ 5 — and aggregates with min. The via coin 5 branch reaches the base case dp[0] = 0 in a single hop and contributes 1, beating the two-coin branches. The arrows are dependency arrows (dp[5] depends on dp[4], dp[3], dp[0]). A correct iteration order computes dp[0], dp[3], dp[4] before dp[5]. In the table fill, this is automatic because we sweep a = 0, 1, 2, ..., 11 in order. The diagram also visualizes why the recurrence is O(|coins|) per state: one outgoing edge per coin denomination.
13. Common Interview Problems
| Problem | LeetCode # | Variant |
|---|---|---|
| Coin Change | LC 322 | Minimum coins (variant A) |
| Coin Change II | LC 518 | Number of combinations (variant B) |
| Combination Sum IV | LC 377 | Number of permutations (loop swap of B) |
| Perfect Squares | LC 279 | Min “coins” where coins = perfect squares ≤ n |
| Minimum Cost For Tickets | LC 983 | DP with day-budget decisions; coin-like |
| Number of Dice Rolls With Target Sum | LC 1155 | Bounded version (each coin used ≤ k times) |
| Word Break | LC 139 | Same loop structure as variant A but boolean |
| Combination Sum | LC 39 | Backtracking (enumerates combinations explicitly) — useful contrast |
14. Open Questions
- Is there a closed-form (rather than algorithmic) characterization of canonical denomination sets for arbitrary
n? Closed-form rules are known for 2-, 3-, 4-, 5-, and (per Cai et al. 2021) 6-coin systems by case analysis, but no uniform rule for generalnis known — Pearson’sO(n³)test remains the best general tool. - For the bounded case (each coin at most
k_itimes), the DP becomes 2Ddp[i][a]and the rolling-array trick requires more care. When is the bounded case sub-quadratic? - Are there approximation algorithms (FPTAS — Fully Polynomial-Time Approximation Scheme) for change-making? Subset-sum has one (Ibarra & Kim 1975); change-making’s status is similar.
- When the amount is astronomically large (
10^18) but|coins|is tiny, does the problem admit a number-theoretic shortcut (e.g., via the Frobenius number / Chicken McNugget theorem)?
15. See Also
- Memoization vs Tabulation — the foundational DP framework
- DP State Identification — recognition recipe; coin change is a canonical “amount-as-state” example
- Unbounded Knapsack — direct generalization (each coin = item with weight = denomination); same
O(n · W)1D DP - 01 Knapsack — bounded sibling; iteration-order contrast (capacity descending vs ascending)
- Partition Equal Subset Sum — feasibility variant of subset-sum
- Climbing Stairs — degenerate variant B (number-of-permutations form) where the only “coins” are 1 and 2; same recurrence, different aggregation
- Fibonacci DP — climbing-stairs and coin-change-with-coins-1-and-2 both reduce to Fibonacci
- Greedy Algorithms — Proof Techniques — exchange-argument and matroid framework; coin change is the canonical greedy-fails example
- Big-O Notation — pseudo-polynomial complexity
- SWE Interview Preparation MOC