Generated by Codex with GPT-5
Quick facts
- Difficulty:
MEDIUM - Problem: Coin Change
- Main tags:
Array,Dynamic Programming,Breadth-First Search
What the problem is really asking
The input gives a set of coin denominations and a target amount. Each coin can be used as many times as needed.
The goal is not to count how many different ways the amount can be formed. The goal is to find the fewest coins needed to reach the exact amount. If the amount cannot be formed exactly, the answer is -1.
That means the real question is:
if the last coin used were c, what is the best answer for amount - c?
Once the problem is phrased that way, it becomes a shortest-path style dynamic programming problem over smaller amounts.
Why the greedy instinct fails
Many candidates first try “always take the largest coin that fits.” That feels natural, but it is not generally correct.
A simple counterexample is coins = [1, 3, 4] and amount = 6.
- A greedy choice takes
4, then1, then1, for a total of3coins. - The optimal answer is
3 + 3, which uses only2coins.
So the algorithm must compare multiple ways to build the same amount rather than locking into a locally best-looking coin.
The key DP insight
Let min_coins[x] mean:
the minimum number of coins needed to make amount x
Then:
min_coins[0] = 0because zero coins are needed to make amount zero.- For every positive amount
x, try every coincsuch thatc <= x. - If
x - cis reachable, then one valid way to makexis to take the best solution forx - cand add coinc.
That gives the recurrence:
min_coins[x] = 1 + min(min_coins[x - c]) over all usable coins c
This works because every valid solution for x must end with some final coin. Once that final coin is fixed, the rest of the problem is exactly the same problem on a smaller amount.
How to derive the bottom-up solution in an interview
The clean derivation is:
- Start with the recursive thought: “what could the last coin be?”
- Notice that answers for the same smaller amount get reused many times.
- Replace repeated recursion with an array from
0up toamount. - Fill the array left to right so every subproblem is already solved when it is needed.
This yields a O(amount * len(coins)) solution with O(amount) extra space, which is the standard optimal answer for the usual constraints of this problem.
Why this recurrence is correct
For any target amount x, every valid solution must choose some last coin c. Removing that last coin leaves amount x - c. If the best way to make x - c uses k coins, then using coin c last makes a candidate solution for x with k + 1 coins.
Because the algorithm tries every possible last coin, it cannot miss the optimal solution. Because it only builds candidates from already-correct smaller answers, it never invents an invalid one. That is exactly why dynamic programming fits this problem so well.
Python solution
The implementation below keeps the interface simple for LeetCode, but the logic is split into small helpers so the intent stays clear. It normalizes the coin list, builds the DP table once, and returns -1 when the target amount is unreachable.
from math import inf
from typing import Iterable, List
def normalize_denominations(denominations: Iterable[int]) -> list[int]:
"""
Return sorted, unique, positive coin denominations.
The LeetCode input already satisfies the problem constraints, but normalizing
the values keeps the core DP logic simpler and more defensive.
"""
return sorted({value for value in denominations if value > 0})
def compute_minimum_coin_count(denominations: list[int], target_amount: int) -> int:
"""
Return the fewest coins needed to reach the target amount.
If the target amount cannot be formed exactly, return -1.
"""
if target_amount == 0:
return 0
if not denominations:
return -1
# min_coins_for_amount[x] stores the best known answer for amount x.
min_coins_for_amount = [0] + [inf] * target_amount
for current_amount in range(1, target_amount + 1):
best_count_for_current_amount = inf
for coin_value in denominations:
if coin_value > current_amount:
break
remaining_amount = current_amount - coin_value
candidate_count = min_coins_for_amount[remaining_amount]
if candidate_count != inf:
best_count_for_current_amount = min(
best_count_for_current_amount,
candidate_count + 1,
)
min_coins_for_amount[current_amount] = best_count_for_current_amount
final_answer = min_coins_for_amount[target_amount]
return -1 if final_answer == inf else int(final_answer)
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
normalized_coins = normalize_denominations(coins)
return compute_minimum_coin_count(normalized_coins, amount)The time complexity is O(amount * number_of_coins) because each amount checks every coin once. The space complexity is O(amount) for the DP array.
Interview follow-ups
How would you return the actual set of coins, not just the count?
Store one extra array alongside the DP table to remember which coin produced the best answer for each amount. After the table is filled, start from amount and repeatedly subtract the chosen coin until reaching zero. This works because the DP table already proves that each recorded choice leads to an optimal smaller subproblem. The complexity stays O(amount * number_of_coins) time and O(amount) space, with only a small extra reconstruction cost at the end.
Could this be solved with BFS instead of dynamic programming?
Yes. Treat each reachable amount as a graph node, and from amount x add edges to x + coin for every denomination that stays within the target. Then BFS finds the minimum number of coins because every edge represents using exactly one more coin. This is a valid shortest-path formulation, and it can be intuitive in interviews. The tradeoff is that it usually has more bookkeeping than the one-dimensional DP array, so DP is typically the cleaner implementation in Python.
What changes if each coin can be used at most once?
Then this is no longer the unbounded version of the problem. The state must reflect that each coin can be chosen only once, which turns it into a 0/1 knapsack-style dynamic programming problem. A common approach is to iterate the amounts backward for each coin so one coin is not reused multiple times in the same pass. The core idea still relies on smaller subproblems, but the transition order matters much more because reuse is no longer allowed.
What if the question asked for the number of different combinations instead of the minimum number of coins?
That is a different DP definition. Instead of storing the best count of coins, store how many ways there are to build each amount. The recurrence adds counts rather than taking a minimum, and the loop order matters if the question wants combinations rather than permutations. This is why Coin Change and Coin Change II feel related but require different state meanings and different update logic.