Problem
Given a list of coin denominations and a target amount, return the fewest number of coins needed to reach that amount exactly. You can use each denomination as many times as you want. Return -1 if no combination of coins can reach the target.
Example:
- Input:
coins = [1, 2, 5],amount = 11 - Output:
3 - Explanation: 5 + 5 + 1 = 11 using 3 coins
Counter-example (greedy fails): With coins = [1, 5, 11] and amount = 15, greedily picking the largest coin gives 11 + 1 + 1 + 1 + 1 = 5 coins. The actual minimum is 5 + 5 + 5 = 3 coins.
Intuition
The minimum coins needed for any amount depends only on smaller amounts โ if you know the cheapest way to make amount - coin for every coin, you can compute the answer for amount in one pass. This optimal substructure lets us build a solution table bottom-up from 0 to the target, reusing each result exactly once.
Approach 1 โ Brute Force Recursion
Try every coin at each step and recurse on the remaining amount. Without caching, overlapping subproblems are recomputed exponentially.
Steps:
- Base case: if
remaining == 0, return 0 (no coins needed). - Base case: if
remaining < 0, return infinity (invalid path). - Try each coin, recursing on
remaining - coin. - Return 1 plus the minimum valid recursive result, or -1 if none exist.
1def coin_change(coins: list[int], amount: int) -> int:
2 def min_coins(remaining: int) -> float:
3 if remaining == 0:
4 return 0
5 if remaining < 0:
6 return float('inf')
7 best = float('inf')
8 for coin in coins:
9 result = min_coins(remaining - coin)
10 best = min(best, result + 1)
11 return best
12
13 answer = min_coins(amount)
14 return answer if answer != float('inf') else -1- Time: O(S^n) โ each call branches into n recursive calls, with no caching, over up to S levels
- Space: O(S) โ maximum call stack depth equals
amountdivided by the smallest coin
Approach 2 โ Bottom-Up Dynamic Programming
Build a dp table where dp[i] holds the minimum coins for amount i. Use previously solved smaller amounts to compute each new entry in a single forward pass.
Steps:
- Allocate
dpof sizeamount + 1, initialized toamount + 1as a sentinel (you can never need more coins than the amount itself). - Set
dp[0] = 0โ zero coins are needed to make zero. - For each
targetfrom 1 toamount, and for eachcoin: - If
coin <= target, updatedp[target] = min(dp[target], dp[target - coin] + 1). - Return
dp[amount]if it is still a valid value (โคamount), otherwise -1.
1def coin_change(coins: list[int], amount: int) -> int:
2 dp = [amount + 1] * (amount + 1) # sentinel: valid solutions need at most 'amount' coins
3 dp[0] = 0
4
5 for target in range(1, amount + 1):
6 for coin in coins:
7 if coin <= target:
8 dp[target] = min(dp[target], dp[target - coin] + 1)
9
10 return dp[amount] if dp[amount] <= amount else -1- Time: O(S ร n) โ for each of S amounts, we try all n coin denominations once
- Space: O(S) โ the dp array has one slot per amount from 0 to S
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| Brute Force Recursion | O(S^n) | O(S) | Never in production; useful only to understand why DP is needed |
| Bottom-Up DP | O(S ร n) | O(S) | Always โ no recursion overhead, handles large inputs without stack overflow |
Common Mistakes
- Initializing dp with 0 instead of a sentinel: Every entry except
dp[0]must start at "infinity"; using 0 makes every target appear reachable with 0 coins, breaking all comparisons. - Overflowing when using
Integer.MAX_VALUEas sentinel in Java/C++: Adding 1 toInteger.MAX_VALUEwraps to a negative number, making it look like a valid minimum. Useamount + 1as the sentinel instead. - Off-by-one in dp array size: Allocating
new int[amount]instead ofnew int[amount + 1]putsdp[amount]out of bounds โ the table needs indices 0 throughamountinclusive. - Trusting greedy intuition: Picking the largest coin first fails whenever a smaller combination is cheaper. The classic trap is coins like [1, 5, 11] with amount 15, where greedy picks 11 first but the optimum skips it entirely.
- Returning the sentinel instead of -1: When
dp[amount]still equalsamount + 1after filling the table, the amount is unreachable โ return -1, not the sentinel value.
Related Problems
climbing-stairsโ same unbounded-choice DP recurrence, counting ways to reach a target with steps of fixed sizescoin-change-iiโ same coins/amount setup but counts the number of distinct combinations rather than the minimum countperfect-squaresโ identical DP recurrence with perfect squares playing the role of coin denominationsword-breakโ same "can we build a target from unlimited reuse of given pieces?" DP applied to stringspartition-equal-subset-sumโ bounded (0/1) variant of the same knapsack family where each item can only be used once