You are given an array of coin values and a target amount of money. Return the minimum number of coins needed to total the target amount. If this isn't possible, return ‐1. You may assume there's an unlimited supply of each coin.
Input: coins = [1, 2, 3], target = 5
Output: 2
Explanation: Use one 2-dollar coin and one 3-dollar coin to make 5 dollars.
Input: coins = [2, 4], target = 5
Output: -1
New to this one? Start here. Below is the core idea in everyday language, plus a real-world analogy—then the sections that follow build on it with the formal reasoning, code, and step-by-step walkthroughs.
A vending machine must return change using the fewest coins possible (fewer coins = less to dispense and reload). With friendly currencies you can grab the biggest coin that fits — but with arbitrary denominations that greedy trick fails (with coins [1, 3, 4] and target 6, greedy gives 4+1+1 = 3 coins, but 3+3 = 2 is better). To always be right, we consider every coin at each step — that is DP.
To make target, the last coin you added was some coins[i]. Whatever it was, the rest must make target − coins[i] in as few coins as possible. So best(target) = 1 + min over coins of best(target − coins[i]). Solve small targets first and the big one falls out.
In this problem, there’s no restriction on the number of coins we can use, which makes a brute force approach that tries every possible coin combination impossible, due to the infinite number of possible combinations. This indicates the need for a more efficient method.
Consider the example below:
If we use a 3-dollar coin from the array, then we’ll only need 2 dollars more to make 5. This gives us a new target: find the fewest number of coins needed to make 2 dollars:
This indicates we’ve identified subproblems within the main problem, where each subproblem requires finding the fewest number of coins needed to make a smaller target.
Each coin we use creates a new subproblem. For example, using a 1-dollar coin changes our target from 5 to 4 dollars. Let's visualize how these smaller targets, representing new subproblems, are created after using each coin:
In extension, each of these subproblems can be solved by breaking them down into further subproblems:
A path that ends with a target of 0 means the coins used in that path add up to 5. If the target becomes negative, it means the path is invalid, so we should stop extending the path.
We’ve observed how new subproblems are created, but haven’t yet addressed how to attain the solutions to them. Remember, each subproblem needs to return the minimum number of coins needed to reach its target.
Consider the main problem with a target of 5. To solve this, we first need to find the minimum number of coins needed to reach each of its three subproblems. The solution to the main problem is the smallest result among these subproblems, plus 1, to account for the coin used to create the subproblem. This highlights an optimal substructure in the problem, allowing us to define the following recurrence relation:
min_coin_combination(target) = 1 + min(min_coin_combination(target - coin_i) | coin_i ∈ coins)
Base case
Naturally, we need a base case for this formula. The base case occurs when the target equals 0, which is the simplest version of this problem, as no coins are needed to meet the target. In this case, we return 0.
Memoization
An important thing to notice is that we might end up solving the same subproblem multiple times. For instance, we calculate the subproblem target = 3 two times in the previous example:
This highlights the existence of overlapping subproblems. Memoization improves our solution by storing the solutions to subproblems as they are computed, ensuring each subproblem is solved only once. This eliminates redundant calculations, and can significantly reduce the size of the recursion tree:
def min_coin_combination_top_down(coins: List[int], target: int) -> int:
res = top_down_dp(coins, target, {})
return -1 if res == float('inf') else res
def top_down_dp(coins: List[int], target: int, memo: Dict[int, int]) -> int:
# Base case: if the target is 0, then 0 coins are needed to reach it.
if target == 0:
return 0
if target in memo:
return memo[target]
# Initialize 'min_coins' to a large number.
min_coins = float('inf')
for coin in coins:
# Avoid negative targets.
if coin <= target:
# Calculate the minimum number of coins needed if we use the current coin.
min_coins = min(min_coins, 1 + top_down_dp(coins, target - coin, memo))
memo[target] = min_coins
return memo[target]#include <vector>
using namespace std;
const int INF = 1000000000; // stands in for "impossible"
int minCoinsFrom(vector<int>& coins, int target) {
if (target == 0) { // base case: 0 coins make 0
return 0;
}
int best = INF;
for (int i = 0; i < (int)coins.size(); i++) {
if (coins[i] <= target) { // this coin fits
int sub = minCoinsFrom(coins, target - coins[i]);
if (sub != INF && sub + 1 < best) { // using this coin is better
best = sub + 1;
}
}
}
return best;
}
int minCoinCombinationTopDown(vector<int>& coins, int target) {
int result = minCoinsFrom(coins, target);
return result == INF ? -1 : result;
}// These methods live inside a class, e.g. class Solution { ... }
static final int INF = 1000000000; // stands in for "impossible"
int minCoinCombinationTopDown(int[] coins, int target) {
int result = minCoinsFrom(coins, target);
return result == INF ? -1 : result;
}
int minCoinsFrom(int[] coins, int target) {
if (target == 0) { // base case: 0 coins make 0
return 0;
}
int best = INF;
for (int i = 0; i < coins.length; i++) {
if (coins[i] <= target) { // this coin fits
int sub = minCoinsFrom(coins, target - coins[i]);
if (sub != INF && sub + 1 < best) { // using this coin is better
best = sub + 1;
}
}
}
return best;
}Time complexity:
Without memoization, the time complexity of min_coin_combination_top_down would be , where denotes the number of coins, and denotes the smallest coin value. The recursion tree has a branch factor of because we make a recursive call for up to coins. The depth of the tree is because in the worst case, we continually reduce the target value by the smallest coin.
With memoization, each subproblem is solved only once. Since there are at most subproblems, and we iterate through all coins for each subproblem, the time complexity is .
Space complexity: The space complexity is because, while the maximum depth of the recursive call stack is only , the memoization array stores up to key-value pairs.
Using the same technique discussed in the Climbing Stairs problem, we can convert our top-down solution to a bottom-up one by translating the memoization array to a DP array.
First, let’s look at the value our memoization array stores, as shown in the following code snippet of the top-down implementation:
for coin in coins:
if coin <= target:
min_coins = min(min_coins, 1 + top_down_dp(coins, target - coin, memo))
memo[target] = min_coins
Translating this to a DP array provides the following code:
for coin in coins:
if coin <= target:
dp[target] = min(dp[target], 1 + dp[target - coin])
This code snippet only includes the calculation for one target value. In our top-down solution, this calculation is repeated for every target value from the initial target, down to the base case (target == 0).
In the bottom-up solution, we need to reverse this order by starting with the base case and working our way up to the initial target value (hence the name “bottom-up”). This is necessary because our DP array calculation depends on the DP values of smaller targets. So, we need to calculate the answers for smaller targets first. This can be done using a for-loop from 1 to the target (starting at 1 since the base case of 0 is already set):
for t in range(1, target + 1):
for coin in coins:
if coin <= t:
dp[t] = min(dp[t], 1 + dp[t - coin])
Once this is done, the answer to the problem will be stored in dp[target].
def min_coin_combination_bottom_up(coins: List[int], target: int) -> int:
# The DP array will store the minimum number of coins needed for each amount. Set
# each element to a large number initially.
dp = [float('inf')] * (target + 1)
# Base case: if the target is 0, then 0 coins are needed.
dp[0] = 0
# Update the DP array for all target amounts greater than 0.
for t in range(1, target + 1):
for coin in coins:
if coin <= t:
dp[t] = min(dp[t], 1 + dp[t - coin])
return dp[target] if dp[target] != float('inf') else -1#include <vector>
using namespace std;
int minCoinCombinationBottomUp(vector<int>& coins, int target) {
const int INF = 1000000000;
// dp[t] = fewest coins to make amount t; start everything at "infinity".
vector<int> dp(target + 1, INF);
dp[0] = 0; // base case: 0 coins make 0
for (int t = 1; t <= target; t++) {
for (int i = 0; i < (int)coins.size(); i++) {
int coin = coins[i];
if (coin <= t && dp[t - coin] != INF && dp[t - coin] + 1 < dp[t]) {
dp[t] = dp[t - coin] + 1;
}
}
}
return dp[target] == INF ? -1 : dp[target];
}// These methods live inside a class, e.g. class Solution { ... }
int minCoinCombinationBottomUp(int[] coins, int target) {
final int INF = 1000000000;
// dp[t] = fewest coins to make amount t; start everything at "infinity".
int[] dp = new int[target + 1];
Arrays.fill(dp, INF);
dp[0] = 0; // base case: 0 coins make 0
for (int t = 1; t <= target; t++) {
for (int i = 0; i < coins.length; i++) {
int coin = coins[i];
if (coin <= t && dp[t - coin] != INF && dp[t - coin] + 1 < dp[t]) {
dp[t] = dp[t - coin] + 1;
}
}
}
return dp[target] == INF ? -1 : dp[target];
}Time complexity: The time complexity of min_coin_combination_bottom_up is because we loop through all coins for each value between 1 and .
Space complexity: The space complexity is due to the space occupied by the DP array, which is of size .
Two step-by-step walkthroughs of the algorithm below: a valid case (the expected happy path) and an invalid / edge case that exercises the tricky parts. Each step shows the program state as the code runs—the same steps apply to Python, C++ and Java.
Fill dp left to right. Each cell is 1 + the smallest reachable neighbor dp[t − coin].
dp[0] = 0; the rest start at ∞.
dp[1] = 1 (a 1), dp[2] = 1 (a 2), dp[3] = 1 (a 3).
dp[3]+1, dp[2]+1, dp[1]+1 = 1+1 = 2.
dp[4]+1, dp[3]+1, dp[2]+1 = 1+1 = 2 → answer 2.
| t | candidates 1 + dp[t-coin] | dp[t] |
|---|---|---|
| 1 | 1+dp[0]=1 | 1 |
| 2 | 1+dp[1]=2, 1+dp[0]=1 | 1 |
| 3 | 1+dp[2]=2, 1+dp[1]=2, 1+dp[0]=1 | 1 |
| 4 | 1+dp[3]=2, 1+dp[2]=2, 1+dp[1]=2 | 2 |
| 5 | 1+dp[4]=3, 1+dp[3]=2, 1+dp[2]=2 | 2 → return 2 |
Fewest coins for 5 is 2 (namely 2 + 3). ✓
Every coin is even, so no combination reaches an odd amount — the odd cells stay ∞ forever.
dp[2] = 1, dp[4] = 1; odd amounts 1, 3, 5 never become reachable.
| t | reachable? | dp[t] |
|---|---|---|
| 2 | yes (one 2) | 1 |
| 4 | yes (two 2s or one 4) | 1 |
| 5 | no (odd) | ∞ → return -1 |
Result -1 — the honest “cannot be done” answer. ✗
float('inf') is the sentinel; inf + 1 stays inf, so unreachable amounts never masquerade as small.
Use a big int sentinel (109) and guard with dp[t - coin] != INF so you never do INF + 1.
Same sentinel and guard; Arrays.fill(dp, INF) initializes the table in one line.
Tip: When a problem asks for the minimum or maximum of something, it might be a DP problem.
If you spot keywords like "minimum", "maximum", "longest", or "shortest", in the problem description, consider whether a DP approach might be appropriate, as many DP problems involve optimizing a certain value, such as finding the minimum cost, or longest sequence.