Skip to content

322. Coin Change (Medium)

Problem

Given coins of various denominations and a target amount, return the fewest number of coins summing to amount (or -1 if impossible). Coins may be used unlimited times.

Example

  • coins = [1, 2, 5], amount = 113 (5 + 5 + 1)
  • coins = [2], amount = 3-1
  • coins = [1], amount = 00

LeetCode 322 · Link · Medium

Approach 1: Recursive

f(n) = 1 + min(f(n - c) for c in coins if c ≤ n).

def coin_change(coins, amount):
def f(n): # L1: define recursive helper
if n == 0: # L2: O(1) base case
return 0
if n < 0: # L3: O(1) base case
return float('inf')
best = float('inf') # L4: O(1)
for c in coins: # L5: loop over |coins| denominations
best = min(best, 1 + f(n - c)) # L6: O(1) + recurse
return best
result = f(amount) # L7: kick off recursion
return -1 if result == float('inf') else result # L8: O(1)

Where the time goes, line by line

Variables: n = len(coins), A = the target amount.

LinePer-call costTimes executedContribution
L2, L3 (base cases)O(1)once per callO(1) each
L5 (coin loop)O(1)n per callO(n) per call
L6 (recurse)O(n) per branchbranches up to n^AO(n · n^A) ← dominates

Without memoization every sub-problem is recomputed from scratch. The call tree has up to n choices at each of A levels, giving an exponential blowup.

Complexity

  • Time: O(n^A), driven by L6. Exponential, unusable.
  • Space: O(A) for the recursion stack.

Approach 2: Top-down memoized

from functools import lru_cache
def coin_change(coins, amount):
@lru_cache(maxsize=None)
def f(n): # L1: define memoized helper
if n == 0: # L2: O(1) base case
return 0
if n < 0: # L3: O(1) base case
return float('inf')
best = float('inf') # L4: O(1)
for c in coins: # L5: loop over n denominations
best = min(best, 1 + f(n - c)) # L6: O(1) + cached sub-call
return best
result = f(amount) # L7: kick off recursion
return -1 if result == float('inf') else result # L8: O(1)

Where the time goes, line by line

Variables: n = len(coins), A = the target amount.

LinePer-call costTimes executedContribution
L2, L3 (base cases)O(1)once per unique nO(A) total
L5, L6 (coin loop)O(n) per unique callA unique values of nO(n · A) ← dominates
L7 (initial call)O(1)1O(1)

Each value from 0 to A is computed exactly once. The coin loop inside each call is O(n). Cache lookup and store are O(1) amortized.

Complexity

  • Time: O(n · A), driven by L5/L6.
  • Space: O(A) for the memo table plus O(A) for the call stack.

Approach 3: Bottom-up DP (canonical)

dp[i] = min coins to make i. dp[0] = 0; dp[i] = min(dp[i - c] + 1) over valid coins.

def coin_change(coins, amount):
INF = amount + 1 # L1: sentinel larger than any valid answer
dp = [INF] * (amount + 1) # L2: O(A) init
dp[0] = 0 # L3: base case
for i in range(1, amount + 1): # L4: outer loop, A iterations
for c in coins: # L5: inner loop, n iterations
if c <= i: # L6: O(1) guard
dp[i] = min(dp[i], dp[i - c] + 1) # L7: O(1) recurrence
return dp[amount] if dp[amount] != INF else -1 # L8: O(1)

Where the time goes, line by line

Variables: n = len(coins), A = the target amount.

LinePer-call costTimes executedContribution
L2 (init dp)O(1)A+1O(A)
L3 (base case)O(1)1O(1)
L4 (outer loop)O(1)AO(A)
L5, L7 (inner loop + recurrence)O(1)A · nO(A · n) ← dominates
L8 (return)O(1)1O(1)

The double loop at L4/L5 is the whole story. Every (amount, coin) pair is visited exactly once, and L7 is O(1) table lookup plus comparison. No inner recursion, no re-scanning; the DP order guarantees dp[i - c] is already filled when we need it.

Complexity

  • Time: O(A · n), driven by L5/L7 (the double loop).
  • Space: O(A) for the dp table.

Unbounded vs. 0/1 knapsack

Coin Change is unbounded, each coin can be used infinitely. The outer loop over amounts lets the DP “re-use” a coin naturally. Compare with 0/1 knapsack (problem 416 Partition Equal Subset Sum), where each item is used at most once and loop order matters.

Summary

ApproachTimeSpace
Naive recursionO(n^A)O(A)
Top-down memoO(A · n)O(A)
Bottom-up DPO(A · n)O(A)

Template for “min operations to reach target” under free reuse: Coin Change, Perfect Squares, Minimum Cost For Tickets.

Test cases

# Quick smoke tests, paste into a REPL or save as test_322.py and run.
# Uses the canonical implementation (Approach 3: Bottom-up DP).
def coin_change(coins, amount):
INF = amount + 1
dp = [INF] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for c in coins:
if c <= i:
dp[i] = min(dp[i], dp[i - c] + 1)
return dp[amount] if dp[amount] != INF else -1
def _run_tests():
assert coin_change([1, 2, 5], 11) == 3 # 5+5+1, LeetCode example
assert coin_change([2], 3) == -1 # impossible
assert coin_change([1], 0) == 0 # zero amount
assert coin_change([1], 1) == 1 # single coin exact match
assert coin_change([2, 5, 10, 1], 27) == 4 # 10+10+5+2
assert coin_change([186, 419, 83, 408], 6249) == 20
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Arrays, DP table indexed by amount