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
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.
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.
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.
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.
Before attempting this problem, you should be comfortable with:
This is the pure recursive brute-force approach.
For a given amount, we try every coin:
amount - coinWe 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.
dfs(amount):amount == 0, return 0 (no coins needed)res as a very large valueamount - coin >= 01 + dfs(amount - coin)res with the minimumres-1, else return the resultclass 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 minCoinsWhere is the length of the array and is the given .
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
memo to store results for already computed amountsdfs(amount):amount == 0, return 0amount exists in memo, return itamount - coin >= 01 + dfs(amount - coin)memo[amount]-1class 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 minCoinsWhere is the length of the array and is the given .
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:
a - coin,a using 1 extra coin.So each amount depends on previously solved smaller amounts.
dp where dp[a] = minimum coins needed to make amount adp[0] = 0 (0 coins to make amount 0)amount + 1)a from 1 to amount:c:a - c >= 0dp[a] = min(dp[a], 1 + dp[a - c])dp[amount] is still large, return -1dp[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 -1Where is the length of the array and is the given .
Think of each amount as a node in a graph.
x, you can go to x + coin for every coin.0 to amount.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:
amount, we've used the minimum coins.amount == 0, return 00 (starting amount)seen array to avoid revisiting amountssteps = 0steps (represents number of coins used)current + coin == amount, return stepsamount, return -1class 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 -1Where is the length of the array and is the given .
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.
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 -1When 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);