DSAPrep
Medium1-D DP

Coin Change

You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money.

Return the fewest number of coins needed to make up that amount. If that amount cannot be made up by any combination of the coins, return -1. You may assume an infinite number of each coin.

Example 1

Input: coins = [1,2,5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1.

Example 2

Input: coins = [2], amount = 3
Output: -1

Example 3

Input: coins = [1], amount = 0
Output: 0

Constraints

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4
View original on LeetCode ↗

Greedily grabbing the largest coin first does not always work (e.g. coins [1,6,7,9,11], amount 13 – greedy picks 11+1+1 but 6+7 is better), so this needs real search. Define dp[i] as the fewest coins needed to make amount i. To make i, the last coin used was some c in coins, and the rest of the amount, i - c, was made optimally by dp[i-c] coins. Trying every possible last coin and taking the best gives dp[i] = 1 + min(dp[i-c] for c in coins if c <= i).

Top-Down Memoization

Time O(amount · coins)Space O(amount)

Recurse on the remaining amount: solve(rem) is the fewest coins to make rem, trying every coin as the “next” (really: last) coin used.

class Solution:
def coinChange(self, coins: list[int], amount: int) -> int:
memo = {}
def solve(rem: int) -> int:
if rem == 0:
return 0
if rem < 0:
return float('inf')
if rem in memo:
return memo[rem]
best = min((1 + solve(rem - c) for c in coins), default=float('inf'))
memo[rem] = best
return best
result = solve(amount)
return result if result != float('inf') else -1

Without memoization, each call branches into len(coins) more calls down to depth amount, giving an exponential blow-up. Memoizing collapses this to amount distinct subproblems, each doing O(coins) work → O(amount · coins) time, O(amount) space for the memo and recursion stack.

Bottom-Up DP

OptimalTime O(amount · coins)Space O(amount)

Fill dp[0..amount] left to right. dp[0] = 0 (zero coins needed for zero amount). For each i, try every coin c <= i as the last coin used, and take the minimum of dp[i-c] + 1. Cells that stay unreachable represent amounts that cannot be made.

class Solution:
def coinChange(self, coins: list[int], amount: int) -> int:
INF = float('inf')
dp = [0] + [INF] * amount
for i in range(1, amount + 1):
for c in coins:
if i - c >= 0:
dp[i] = min(dp[i], dp[i - c] + 1)
return dp[amount] if dp[amount] != INF else -1

Trace for coins = [1, 2, 5], amount = 11 (only the winning coin choice is shown as a pointer at each step; ties with other coins are noted):

0
0
·
1
·
2
·
3
·
4
·
5
·
6
·
7
·
8
·
9
·
10
·
11
1 / 12
comparingseenresult

dp[0] = 0: zero coins needed to make amount 0.

For each of the amount cells, every coin is tried once → O(amount · coins) time. The dp array costs O(amount) space. This is optimal in the sense that any correct DP must consider every reachable amount up to the target.