322. Coin Change - Explanation
Description
You are given an integer array coins representing coins of different denominations (e.g. 1 dollar, 5 dollars, etc) and an integer amount representing a target amount of money.
Return the fewest number of coins that you need to make up the exact target amount. If it is impossible to make up the amount, return -1.
You may assume that you have an unlimited number of each coin.
Example 1:
Input: coins = [1,5,10], amount = 12
Output: 3Explanation: 12 = 10 + 1 + 1. Note that we do not have to use every kind coin available.
Example 2:
Input: coins = [2], amount = 3
Output: -1Explanation: The amount of 3 cannot be made up with coins of 2.
Example 3:
Input: coins = [1], amount = 0
Output: 0Explanation: Choosing 0 coins is a valid way to make up 0.
Constraints:
1 <= coins.length <= 101 <= coins[i] <= 2^31 - 10 <= amount <= 10000
Topics
Recommended Time & Space Complexity
You should aim for a solution with O(n * t) time and O(t) space, where n is the number of coins and t is the given amount.
Hint 1
Think of this problem in terms of recursion and try to visualize the decision tree, as there are multiple choices at each step. We start with the given amount. At each step of recursion, we have n coins and branch into paths using coins that are less than or equal to the current amount. Can you express this in terms of a recurrence relation? Also, try to determine the base condition to stop the recursion.
Hint 2
If the amount is 0, we return 0 coins. The recurrence relation can be expressed as min(1 + dfs(amount - coins[i])), where we return the minimum coins among all paths. This results in an O(n ^ t) solution, where n is the number of coins and t is the total amount. Can you think of a better approach? Perhaps consider the repeated work and find a way to avoid it.
Hint 3
We can use memoization to avoid the repeated work of calculating the result for each recursive call. A hash map or an array of size t can be used to cache the computed values for a specific amount. At each recursion step, we iterate over every coin and extend only the valid paths. If a result has already been computed, we return it from the cache instead of recalculating it.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Recursion - Exploring all possible coin choices by reducing the problem to smaller subproblems
- Dynamic Programming (Memoization) - Storing results of overlapping subproblems to optimize recursive solutions
- Dynamic Programming (Tabulation) - Building the minimum coins array from smaller amounts to larger
- Breadth-First Search (BFS) - Treating amounts as graph nodes to find the shortest path (minimum coins)
1. Recursion
Intuition
This is the pure recursive brute-force approach.
For a given amount, we try every coin:
- Pick one coin
- Solve the remaining subproblem
amount - coin - Take the minimum coins needed among all choices
We explore all possible combinations, which leads to many repeated subproblems and exponential time — this solution is correct but inefficient.
If no combination reaches exactly 0, we treat it as invalid using a very large number.
Algorithm
- Define a recursive function
dfs(amount):- If
amount == 0, return0(no coins needed)
- If
- Initialize
resas a very large value - For each coin:
- If
amount - coin >= 0- Recursively compute
1 + dfs(amount - coin) - Update
reswith the minimum
- Recursively compute
- If
- Return
res - If the final result is still very large, return
-1, else return the result
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
def dfs(amount):
if amount == 0:
return 0
res = 1e9
for coin in coins:
if amount - coin >= 0:
res = min(res, 1 + dfs(amount - coin))
return res
minCoins = dfs(amount)
return -1 if minCoins >= 1e9 else minCoinsTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the array and is the given .
2. Dynamic Programming (Top-Down)
Intuition
This is the optimized version of the brute-force recursion using memoization.
The key observation is that the same amount gets solved multiple times in recursion.
So instead of recomputing it, we store the result the first time and reuse it.
Each amount represents a subproblem:
Minimum coins needed to make this amount
Algorithm
- Use a hashmap
memoto store results for already computed amounts - Define
dfs(amount):- If
amount == 0, return0 - If
amountexists inmemo, return it
- If
- Try every coin:
- If
amount - coin >= 0- Compute
1 + dfs(amount - coin) - Track the minimum
- Compute
- If
- Store the result in
memo[amount] - Return the stored value
- If the final answer is still very large, return
-1
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
memo = {}
def dfs(amount):
if amount == 0:
return 0
if amount in memo:
return memo[amount]
res = 1e9
for coin in coins:
if amount - coin >= 0:
res = min(res, 1 + dfs(amount - coin))
memo[amount] = res
return res
minCoins = dfs(amount)
return -1 if minCoins >= 1e9 else minCoinsTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the array and is the given .
3. Dynamic Programming (Bottom-Up)
Intuition
This is the bottom-up DP version of Coin Change.
Instead of asking “how many coins to make this amount?” recursively, we build answers from smaller amounts to larger ones.
Key idea:
- If we know the minimum coins to make
a - coin, - then we can make
ausing 1 extra coin.
So each amount depends on previously solved smaller amounts.
Algorithm
- Create a DP array
dpwheredp[a]= minimum coins needed to make amounta - Initialize:
dp[0] = 0(0 coins to make amount 0)- All other values as a large number (
amount + 1)
- For every amount
afrom1toamount:- For each coin
c:- If
a - c >= 0- Update
dp[a] = min(dp[a], 1 + dp[a - c])
- Update
- If
- For each coin
- If
dp[amount]is still large, return-1 - Otherwise return
dp[amount]
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [amount + 1] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if a - c >= 0:
dp[a] = min(dp[a], 1 + dp[a - c])
return dp[amount] if dp[amount] != amount + 1 else -1Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the array and is the given .
4. Breadth First Search
Intuition
Think of each amount as a node in a graph.
- From a current amount
x, you can go tox + coinfor every coin. - Each edge represents using one coin.
- We want the minimum number of coins, which means the shortest path from
0toamount.
This makes the problem a shortest path in an unweighted graph, so Breadth First Search (BFS) is a natural fit.
BFS explores level by level:
- Level 1 → amounts reachable using 1 coin
- Level 2 → amounts reachable using 2 coins
- First time we reach
amount, we've used the minimum coins.
Algorithm
- If
amount == 0, return0 - Initialize a queue with
0(starting amount) - Use a
seenarray to avoid revisiting amounts - Set
steps = 0 - While the queue is not empty:
- Increment
steps(represents number of coins used) - For each element in the current level:
- Try adding every coin
- If
current + coin == amount, returnsteps - If within bounds and unseen, mark seen and push into queue
- Increment
- If BFS finishes without reaching
amount, return-1
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
if amount == 0:
return 0
q = deque([0])
seen = [False] * (amount + 1)
seen[0] = True
res = 0
while q:
res += 1
for _ in range(len(q)):
cur = q.popleft()
for coin in coins:
nxt = cur + coin
if nxt == amount:
return res
if nxt > amount or seen[nxt]:
continue
seen[nxt] = True
q.append(nxt)
return -1Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the array and is the given .
Common Pitfalls
Using Greedy Instead of DP
A common mistake is assuming a greedy approach (always picking the largest coin) works. For coins = [1, 3, 4] and amount = 6, greedy picks [4, 1, 1] (3 coins), but the optimal is [3, 3] (2 coins). The problem requires exploring all combinations.
Incorrect Handling of Impossible Cases
Forgetting to return -1 when the amount cannot be formed. Using 0 or leaving the result as infinity/large value causes wrong answers.
# Wrong: returns large number instead of -1
return dp[amount]
# Correct: check if amount is reachable
return dp[amount] if dp[amount] != amount + 1 else -1Integer Overflow When Adding 1 to Infinity
When using INT_MAX or Integer.MAX_VALUE as the "impossible" sentinel, adding 1 causes overflow. Use amount + 1 as the sentinel instead, since you can never need more than amount coins (when using coin value 1).
// Wrong: 1 + INT_MAX overflows
if (result != INT_MAX) res = min(res, 1 + result);
// Correct: use amount + 1 as sentinel
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
Sign in to join the discussion