322. Coin Change
Problem Description
You have an array coins
that contains integers representing different coin denominations (like 1 cent, 5 cents, 25 cents, etc.). You also have a target amount
that represents the total amount of money you need to make.
Your task is to find the minimum number of coins needed to make exactly that amount. You can use each type of coin as many times as you want (infinite supply of each coin denomination).
If it's impossible to make the exact amount with the given coins, return -1
.
For example:
- If
coins = [1, 2, 5]
andamount = 11
, the minimum number of coins is3
(using two 5-cent coins and one 1-cent coin: 5 + 5 + 1 = 11) - If
coins = [2]
andamount = 3
, it's impossible to make exactly 3 with only 2-cent coins, so return-1
The solution uses dynamic programming with a 2D table f[i][j]
where:
i
represents using the firsti
types of coinsj
represents the target amountf[i][j]
stores the minimum number of coins needed
The key state transition is: f[i][j] = min(f[i-1][j], f[i][j-x] + 1)
, which means:
- Either don't use the current coin type (take
f[i-1][j]
) - Or use one of the current coin and look up the solution for the remaining amount (
f[i][j-x] + 1
)
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: We can model this problem as a graph where each node represents an amount of money, and edges represent using a coin to transition from one amount to another.
Is it a tree?
- No: This is not a tree structure because multiple paths can lead to the same amount (e.g., reaching amount 3 using [1,1,1] or [1,2]).
Is the problem related to directed acyclic graphs?
- No: The graph can have cycles (we can use the same coin multiple times).
Is the problem related to shortest paths?
- Yes: We're looking for the minimum (shortest) number of coins (steps) to reach the target amount from 0.
Is the graph Weighted?
- No: Each edge has the same weight of 1 (using one coin counts as one step).
BFS
- Yes: Since we have an unweighted graph shortest path problem, BFS is the appropriate algorithm.
Conclusion: The flowchart suggests using BFS (Breadth-First Search) for finding the minimum number of coins needed. In the BFS approach, we start from amount 0 and explore all possible amounts we can reach by adding one coin at a time, level by level, until we reach the target amount. The first time we reach the target amount gives us the minimum number of coins needed.
Intuition
Think of this problem as finding the shortest path in a graph. Imagine you're at amount 0, and you want to reach the target amount. Each coin represents a way to "jump" forward by its value. The question becomes: what's the minimum number of jumps needed to reach exactly the target?
This naturally leads us to think about BFS because:
- We want the minimum number of steps (coins)
- Each step has equal weight (using any coin counts as 1 coin used)
- We explore all possibilities level by level
However, the provided solution uses Dynamic Programming instead of BFS. Why? While BFS would work, DP is more efficient here because:
The problem has optimal substructure: If we know the minimum coins needed for amount j-x
(where x
is a coin value), then for amount j
, we just need one more coin of value x
. This gives us f[j] = f[j-x] + 1
.
The problem also has overlapping subproblems: To calculate the minimum coins for amount 11, we might need to know the answer for amount 6 (if we use a 5-coin). But amount 6 might also be needed when calculating amount 7 (if we use a 1-coin). Instead of recalculating, we store and reuse these results.
The 2D DP approach f[i][j]
represents: "What's the minimum coins needed to make amount j
using only the first i
types of coins?" For each coin type and amount combination, we have two choices:
- Don't use this coin type at all:
f[i-1][j]
- Use at least one of this coin:
f[i][j-x] + 1
We take the minimum of these choices. The beauty is that f[i][j-x]
already contains the optimal solution for using the same coin multiple times (unbounded knapsack property), avoiding the need to explicitly enumerate how many coins of each type to use.
Solution Approach
The solution implements the Complete Knapsack variant of Dynamic Programming. Let's walk through the implementation step by step:
1. Initialize the DP Table:
f = [[inf] * (n + 1) for _ in range(m + 1)]
f[0][0] = 0
- Create a 2D table
f[i][j]
wherei
ranges from 0 tom
(number of coin types) andj
ranges from 0 ton
(target amount) f[i][j]
represents the minimum coins needed to make amountj
using the firsti
types of coins- Initialize all values to infinity (impossible), except
f[0][0] = 0
(0 coins needed to make amount 0)
2. Fill the DP Table:
for i, x in enumerate(coins, 1):
for j in range(n + 1):
f[i][j] = f[i - 1][j]
if j >= x:
f[i][j] = min(f[i][j], f[i][j - x] + 1)
For each coin type i
with value x
:
- For each amount
j
from 0 ton
:- First, inherit the solution without using this coin:
f[i][j] = f[i-1][j]
- If the current amount
j
is at least the coin valuex
, we can consider using this coin:f[i][j - x] + 1
means: take the minimum coins for amount(j - x)
using coins up to typei
, then add 1 more coin of valuex
- Take the minimum between not using and using this coin
- First, inherit the solution without using this coin:
3. State Transition Formula:
The key insight is the state transition: f[i][j] = min(f[i-1][j], f[i][j-x] + 1)
This elegantly handles the unbounded knapsack nature because:
f[i-1][j]
: Don't use coin typei
at allf[i][j-x] + 1
: Use at least one coin of typei
. Note that we usef[i][j-x]
notf[i-1][j-x]
, which means we can use the same coin type multiple times
4. Return the Result:
return -1 if f[m][n] >= inf else f[m][n]
- If
f[m][n]
is still infinity, it's impossible to make the exact amount, return -1 - Otherwise, return the minimum number of coins needed
Time Complexity: O(m * n)
where m
is the number of coin types and n
is the target amount
Space Complexity: O(m * n)
for the 2D DP table
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with coins = [1, 2, 5]
and amount = 7
.
We'll build a 2D DP table f[i][j]
where:
- Rows (i): represent using the first i coin types (0 to 3)
- Columns (j): represent the target amount (0 to 7)
Initial Setup:
Coins: [1, 2, 5] Amount: 7 Initialize f[4][8] with all infinity, except f[0][0] = 0
Step-by-step DP Table Construction:
Row 0 (using no coins):
Amount: 0 1 2 3 4 5 6 7 f[0][j]: 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞
Row 1 (using coin value 1):
- For j=0: f[1][0] = f[0][0] = 0
- For j=1: f[1][1] = min(f[0][1], f[1][0]+1) = min(∞, 0+1) = 1
- For j=2: f[1][2] = min(f[0][2], f[1][1]+1) = min(∞, 1+1) = 2
- For j=3: f[1][3] = min(f[0][3], f[1][2]+1) = min(∞, 2+1) = 3
- Continue this pattern...
Amount: 0 1 2 3 4 5 6 7 f[1][j]: 0 1 2 3 4 5 6 7
Row 2 (using coins 1 and 2):
- For j=0: f[2][0] = f[1][0] = 0
- For j=1: f[2][1] = f[1][1] = 1 (can't use coin 2, j < 2)
- For j=2: f[2][2] = min(f[1][2], f[2][0]+1) = min(2, 0+1) = 1
- For j=3: f[2][3] = min(f[1][3], f[2][1]+1) = min(3, 1+1) = 2
- For j=4: f[2][4] = min(f[1][4], f[2][2]+1) = min(4, 1+1) = 2
- Continue this pattern...
Amount: 0 1 2 3 4 5 6 7 f[2][j]: 0 1 1 2 2 3 3 4
Row 3 (using coins 1, 2, and 5):
- For j=0-4: Same as f[2][j] (can't use coin 5 for amounts < 5)
- For j=5: f[3][5] = min(f[2][5], f[3][0]+1) = min(3, 0+1) = 1
- For j=6: f[3][6] = min(f[2][6], f[3][1]+1) = min(3, 1+1) = 2
- For j=7: f[3][7] = min(f[2][7], f[3][2]+1) = min(4, 1+1) = 2
Amount: 0 1 2 3 4 5 6 7 f[3][j]: 0 1 1 2 2 1 2 2
Final Answer: f[3][7] = 2
This means we need minimum 2 coins to make amount 7. The actual coins would be [5, 2] (one 5-coin and one 2-coin).
Key Observations:
- When processing coin value 2, we improved solutions for even amounts (j=2,4,6)
- When processing coin value 5, we dramatically improved j=5 from 3 coins to just 1 coin
- The formula f[i][j-x] + 1 allows us to reuse the same coin type multiple times (notice how f[2][4] uses f[2][2], allowing two 2-coins)
Solution Implementation
1class Solution:
2 def coinChange(self, coins: List[int], amount: int) -> int:
3 # Initialize dimensions: number of coin types and target amount
4 num_coins = len(coins)
5
6 # Create 2D DP table
7 # dp[i][j] represents minimum coins needed to make amount j using first i coin types
8 dp = [[float('inf')] * (amount + 1) for _ in range(num_coins + 1)]
9
10 # Base case: 0 coins needed to make amount 0
11 dp[0][0] = 0
12
13 # Fill the DP table
14 for coin_idx in range(1, num_coins + 1):
15 current_coin_value = coins[coin_idx - 1]
16
17 for current_amount in range(amount + 1):
18 # Option 1: Don't use the current coin type
19 dp[coin_idx][current_amount] = dp[coin_idx - 1][current_amount]
20
21 # Option 2: Use the current coin if possible
22 if current_amount >= current_coin_value:
23 # Compare with using one more of the current coin
24 dp[coin_idx][current_amount] = min(
25 dp[coin_idx][current_amount],
26 dp[coin_idx][current_amount - current_coin_value] + 1
27 )
28
29 # Return result: -1 if impossible, otherwise the minimum number of coins
30 return -1 if dp[num_coins][amount] == float('inf') else dp[num_coins][amount]
31
1class Solution {
2 public int coinChange(int[] coins, int amount) {
3 // Define a large value to represent impossible states (infinity)
4 final int INF = 1 << 30;
5
6 // Get the number of coin types and target amount
7 int numCoins = coins.length;
8 int targetAmount = amount;
9
10 // Create a 2D DP table
11 // dp[i][j] represents the minimum number of coins needed to make amount j
12 // using only the first i types of coins
13 int[][] dp = new int[numCoins + 1][targetAmount + 1];
14
15 // Initialize all states as impossible (infinity)
16 for (int[] row : dp) {
17 Arrays.fill(row, INF);
18 }
19
20 // Base case: 0 coins needed to make amount 0
21 dp[0][0] = 0;
22
23 // Fill the DP table
24 for (int i = 1; i <= numCoins; i++) {
25 for (int j = 0; j <= targetAmount; j++) {
26 // Option 1: Don't use the current coin type
27 // Inherit the result from using only the first (i-1) coin types
28 dp[i][j] = dp[i - 1][j];
29
30 // Option 2: Use the current coin type if possible
31 // Check if current amount j is at least as large as the coin value
32 if (j >= coins[i - 1]) {
33 // Take minimum between not using current coin and using it
34 // When using current coin: add 1 to the count and reduce amount by coin value
35 dp[i][j] = Math.min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
36 }
37 }
38 }
39
40 // Return the result
41 // If the final value is still infinity, it means the amount cannot be formed
42 return dp[numCoins][targetAmount] >= INF ? -1 : dp[numCoins][targetAmount];
43 }
44}
45
1class Solution {
2public:
3 int coinChange(vector<int>& coins, int amount) {
4 int numCoins = coins.size();
5
6 // dp[i][j] represents the minimum number of coins needed to make amount j
7 // using the first i types of coins
8 int dp[numCoins + 1][amount + 1];
9
10 // Initialize with a large value (infinity)
11 // 0x3f3f3f3f is commonly used as infinity in competitive programming
12 memset(dp, 0x3f, sizeof(dp));
13
14 // Base case: 0 coins are needed to make amount 0
15 dp[0][0] = 0;
16
17 // Fill the dp table
18 for (int i = 1; i <= numCoins; ++i) {
19 for (int j = 0; j <= amount; ++j) {
20 // Option 1: Don't use the current coin type
21 dp[i][j] = dp[i - 1][j];
22
23 // Option 2: Use the current coin type (if possible)
24 if (j >= coins[i - 1]) {
25 // We can use the same coin type multiple times (unbounded knapsack)
26 // So we check dp[i][j - coins[i - 1]] instead of dp[i - 1][j - coins[i - 1]]
27 dp[i][j] = min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
28 }
29 }
30 }
31
32 // If the minimum coins needed exceeds the amount itself, it's impossible
33 // Return -1 for impossible cases, otherwise return the minimum coins needed
34 return dp[numCoins][amount] > amount ? -1 : dp[numCoins][amount];
35 }
36};
37
1/**
2 * Finds the minimum number of coins needed to make up the given amount
3 * Uses dynamic programming with 2D array approach
4 *
5 * @param coins - Array of coin denominations
6 * @param amount - Target amount to make change for
7 * @returns Minimum number of coins needed, or -1 if impossible
8 */
9function coinChange(coins: number[], amount: number): number {
10 const numCoins = coins.length;
11 const targetAmount = amount;
12
13 // Initialize DP table
14 // dp[i][j] represents minimum coins needed using first i coin types to make amount j
15 // Initialize with large value (1 << 30) to represent impossible states
16 const dp: number[][] = Array(numCoins + 1)
17 .fill(0)
18 .map(() => Array(targetAmount + 1).fill(1 << 30));
19
20 // Base case: 0 coins needed to make amount 0
21 dp[0][0] = 0;
22
23 // Fill the DP table
24 for (let coinIndex = 1; coinIndex <= numCoins; ++coinIndex) {
25 for (let currentAmount = 0; currentAmount <= targetAmount; ++currentAmount) {
26 // Option 1: Don't use the current coin
27 dp[coinIndex][currentAmount] = dp[coinIndex - 1][currentAmount];
28
29 // Option 2: Use the current coin (if possible)
30 if (currentAmount >= coins[coinIndex - 1]) {
31 dp[coinIndex][currentAmount] = Math.min(
32 dp[coinIndex][currentAmount],
33 dp[coinIndex][currentAmount - coins[coinIndex - 1]] + 1
34 );
35 }
36 }
37 }
38
39 // Return result: -1 if impossible, otherwise the minimum coin count
40 return dp[numCoins][targetAmount] > targetAmount ? -1 : dp[numCoins][targetAmount];
41}
42
Time and Space Complexity
The time complexity is O(m × n)
, where m
is the number of types of coins and n
is the total amount. This is because we have two nested loops: the outer loop iterates through all m
coins (from index 1 to m), and the inner loop iterates through all possible amounts from 0 to n
. Each cell f[i][j]
is computed exactly once with constant time operations.
The space complexity is O(m × n)
. We create a 2D DP table f
with dimensions (m + 1) × (n + 1)
to store the minimum number of coins needed for each subproblem. The table has (m + 1)
rows representing different coin combinations (0 to m coins) and (n + 1)
columns representing different target amounts (0 to n).
Common Pitfalls
1. Incorrect Base Case Initialization
A common mistake is only initializing dp[0][0] = 0
but forgetting that we can make amount 0 with any subset of coins (by using 0 coins). This leads to incorrect results.
Incorrect:
dp = [[float('inf')] * (amount + 1) for _ in range(num_coins + 1)]
dp[0][0] = 0 # Only this is initialized
Correct:
dp = [[float('inf')] * (amount + 1) for _ in range(num_coins + 1)]
# Initialize all dp[i][0] = 0 (0 coins needed for amount 0)
for i in range(num_coins + 1):
dp[i][0] = 0
2. Using Wrong DP State in Transition
When considering using a coin, some people mistakenly use dp[coin_idx - 1][current_amount - current_coin_value] + 1
instead of dp[coin_idx][current_amount - current_coin_value] + 1
. This error treats the problem as a 0/1 knapsack (each coin used at most once) rather than an unbounded knapsack.
Incorrect:
# This assumes each coin can only be used once
dp[coin_idx][current_amount] = min(
dp[coin_idx][current_amount],
dp[coin_idx - 1][current_amount - current_coin_value] + 1 # Wrong!
)
Correct:
# This allows unlimited use of each coin type
dp[coin_idx][current_amount] = min(
dp[coin_idx][current_amount],
dp[coin_idx][current_amount - current_coin_value] + 1 # Correct!
)
3. Space Optimization Gone Wrong
When optimizing to 1D array, a common mistake is updating values in the wrong order, causing previously computed values to be overwritten before they're used.
Incorrect 1D optimization:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for j in range(amount + 1): # Wrong order!
if j >= coin:
dp[j] = min(dp[j], dp[j - coin] + 1)
Correct 1D optimization:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for j in range(coin, amount + 1): # Start from coin value
dp[j] = min(dp[j], dp[j - coin] + 1)
4. Integer Overflow with Large Values
Using a very large number like sys.maxsize
for infinity can cause integer overflow when adding 1 to it.
Problematic:
import sys
dp = [[sys.maxsize] * (amount + 1) for _ in range(num_coins + 1)]
# Later: dp[i][j] = dp[i][j - coin] + 1 # Could overflow!
Better approach:
# Use float('inf') or a reasonable upper bound
dp = [[float('inf')] * (amount + 1) for _ in range(num_coins + 1)]
# OR
MAX_COINS = amount + 1 # Maximum possible coins needed (all 1-cent coins)
dp = [[MAX_COINS] * (amount + 1) for _ in range(num_coins + 1)]
5. Off-by-One Errors in Index Mapping
Confusion between 0-indexed arrays and 1-indexed logic can lead to accessing wrong coin values.
Incorrect:
for coin_idx in range(1, num_coins + 1):
current_coin_value = coins[coin_idx] # IndexError or wrong coin!
Correct:
for coin_idx in range(1, num_coins + 1):
current_coin_value = coins[coin_idx - 1] # Correct mapping
In a binary min heap, the maximum element can be found in:
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!