518. Coin Change II
Problem Description
You have an array of integers coins
that represents different coin denominations and an integer amount
that represents a target sum of money.
Your task is to find the number of different combinations of coins that can sum up to exactly the given amount
.
Key points about the problem:
- You have an infinite supply of each type of coin (you can use any coin denomination as many times as needed)
- You need to count the number of combinations, not permutations (for example, {1, 2} and {2, 1} count as the same combination)
- If it's impossible to make the target amount with any combination of coins, return
0
- The answer is guaranteed to fit in a 32-bit signed integer
For example:
- If
coins = [1, 2, 5]
andamount = 5
, there are 4 ways to make amount 5:- 5 = 5 (one 5-cent coin)
- 5 = 2 + 2 + 1 (two 2-cent coins and one 1-cent coin)
- 5 = 2 + 1 + 1 + 1 (one 2-cent coin and three 1-cent coins)
- 5 = 1 + 1 + 1 + 1 + 1 (five 1-cent coins)
This is a classic coin change combination problem that can be solved using dynamic programming, specifically using the complete knapsack approach where each item (coin) can be used multiple times.
Intuition
To understand how to solve this problem, let's think about it step by step.
Imagine you're trying to make amount j
using coins. You have two choices for each coin type:
- Don't use this coin type at all - then you need to make amount
j
using only the previous coin types - Use at least one of this coin type - then you first use one coin of value
x
, leaving you with amountj - x
to make, which can still use this coin type again (since we have infinite supply)
This leads us to think about dynamic programming. Let's define f[i][j]
as the number of ways to make amount j
using the first i
types of coins.
For each coin type i
with value x
, we can think about all possible quantities we could use: 0, 1, 2, 3, ... up to k
where k * x ≤ j
.
This gives us:
f[i][j] = f[i-1][j] + f[i-1][j-x] + f[i-1][j-2x] + ... + f[i-1][j-kx]
But there's a clever observation here! If we look at f[i][j-x]
, it equals:
f[i][j-x] = f[i-1][j-x] + f[i-1][j-2x] + ... + f[i-1][j-kx]
Notice that this is exactly the tail part of our first equation! So we can simplify:
f[i][j] = f[i-1][j] + f[i][j-x]
This means: the number of ways to make amount j
using the first i
coins equals:
- The number of ways without using the
i
-th coin at all:f[i-1][j]
- Plus the number of ways where we use at least one of the
i
-th coin:f[i][j-x]
The base case is f[0][0] = 1
(one way to make 0 amount using 0 coins: use nothing), and all other f[0][j] = 0
(no way to make positive amount with no coins).
This approach ensures we count combinations, not permutations, because we process coins in a fixed order and decide how many of each type to use, rather than considering different orderings of the same coins.
Learn more about Dynamic Programming patterns.
Solution Approach
Based on our intuition, we implement the dynamic programming solution using a 2D table.
Step 1: Initialize the DP table
We create a 2D array f
with dimensions (m+1) × (n+1)
where:
m
is the number of coin typesn
is the target amountf[i][j]
represents the number of ways to make amountj
using the firsti
types of coins
m, n = len(coins), amount
f = [[0] * (n + 1) for _ in range(m + 1)]
f[0][0] = 1 # Base case: one way to make 0 with 0 coins
Step 2: Fill the DP table
We iterate through each coin type and each possible amount:
for i, x in enumerate(coins, 1): # i goes from 1 to m, x is the coin value
for j in range(n + 1): # j goes from 0 to amount
# First, inherit the ways without using the current coin
f[i][j] = f[i - 1][j]
# If we can use the current coin (j >= x), add those ways
if j >= x:
f[i][j] += f[i][j - x]
The state transition equation we're implementing is:
f[i][j] = f[i-1][j] + f[i][j-x]
whenj >= x
f[i][j] = f[i-1][j]
whenj < x
How the transition works:
f[i-1][j]
: Ways to make amountj
without using thei
-th coin at allf[i][j-x]
: Ways to make amountj-x
using coins 1 toi
(then add one more coini
to reachj
)
Step 3: Return the result
The answer is stored in f[m][n]
, which represents the number of ways to make the target amount using all available coin types.
return f[m][n]
Time Complexity: O(m × n)
where m
is the number of coins and n
is the amount
Space Complexity: O(m × n)
for the 2D DP table
This complete knapsack approach ensures we count each unique combination exactly once, as we process coins in order and decide how many of each type to include.
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 small example with coins = [1, 2]
and amount = 4
.
We'll build a DP table where f[i][j]
represents the number of ways to make amount j
using the first i
types of coins.
Step 1: Initialize the table
- Create a 3×5 table (3 rows for 0,1,2 coins; 5 columns for amounts 0,1,2,3,4)
- Set
f[0][0] = 1
(one way to make 0 with no coins)
Amount: 0 1 2 3 4 Coins 0: 1 0 0 0 0 Coins 1: 0 0 0 0 0 Coins 2: 0 0 0 0 0
Step 2: Process coin 1 (value = 1) For each amount j from 0 to 4:
f[1][0] = f[0][0] = 1
(inherit from above)f[1][1] = f[0][1] + f[1][0] = 0 + 1 = 1
(can use one 1-coin)f[1][2] = f[0][2] + f[1][1] = 0 + 1 = 1
(use two 1-coins)f[1][3] = f[0][3] + f[1][2] = 0 + 1 = 1
(use three 1-coins)f[1][4] = f[0][4] + f[1][3] = 0 + 1 = 1
(use four 1-coins)
Amount: 0 1 2 3 4 Coins 0: 1 0 0 0 0 Coins 1: 1 1 1 1 1 Coins 2: 0 0 0 0 0
Step 3: Process coin 2 (value = 2) For each amount j from 0 to 4:
f[2][0] = f[1][0] = 1
(inherit from above)f[2][1] = f[1][1] = 1
(can't use 2-coin, amount too small)f[2][2] = f[1][2] + f[2][0] = 1 + 1 = 2
(either {1,1} or {2})f[2][3] = f[1][3] + f[2][1] = 1 + 1 = 2
(either {1,1,1} or {2,1})f[2][4] = f[1][4] + f[2][2] = 1 + 2 = 3
(either {1,1,1,1}, {2,1,1}, or {2,2})
Amount: 0 1 2 3 4 Coins 0: 1 0 0 0 0 Coins 1: 1 1 1 1 1 Coins 2: 1 1 2 2 3
Result: f[2][4] = 3
The three combinations to make 4 are:
- Four 1-coins: {1,1,1,1}
- One 2-coin and two 1-coins: {2,1,1}
- Two 2-coins: {2,2}
Notice how the algorithm naturally avoids counting permutations - we process coins in order and decide how many of each type to use, so {2,1,1} and {1,2,1} are counted as the same combination.
Solution Implementation
1class Solution:
2 def change(self, amount: int, coins: List[int]) -> int:
3 """
4 Calculate the number of combinations to make up the amount using given coins.
5 This is the classic "Coin Change II" problem using dynamic programming.
6
7 Args:
8 amount: Target amount to make change for
9 coins: List of coin denominations available
10
11 Returns:
12 Number of combinations that make up the amount
13 """
14 num_coins = len(coins)
15
16 # dp[i][j] represents the number of ways to make amount j using first i coins
17 # Initialize 2D DP table with dimensions (num_coins + 1) x (amount + 1)
18 dp = [[0] * (amount + 1) for _ in range(num_coins + 1)]
19
20 # Base case: there's one way to make amount 0 (use no coins)
21 dp[0][0] = 1
22
23 # Iterate through each coin type
24 for coin_idx in range(1, num_coins + 1):
25 current_coin_value = coins[coin_idx - 1]
26
27 # For each possible amount from 0 to target amount
28 for current_amount in range(amount + 1):
29 # First, inherit the number of ways without using the current coin
30 dp[coin_idx][current_amount] = dp[coin_idx - 1][current_amount]
31
32 # If current coin can be used (amount >= coin value),
33 # add the ways by including this coin
34 if current_amount >= current_coin_value:
35 dp[coin_idx][current_amount] += dp[coin_idx][current_amount - current_coin_value]
36
37 # Return the number of ways to make the target amount using all available coins
38 return dp[num_coins][amount]
39
1class Solution {
2 /**
3 * Calculate the number of combinations to make up the amount using given coins.
4 * This is a classic unbounded knapsack problem using dynamic programming.
5 *
6 * @param amount The target amount to make change for
7 * @param coins Array of coin denominations available
8 * @return Number of combinations that make up the amount
9 */
10 public int change(int amount, int[] coins) {
11 int numCoins = coins.length;
12 int targetAmount = amount;
13
14 // dp[i][j] represents the number of ways to make amount j using first i coins
15 int[][] dp = new int[numCoins + 1][targetAmount + 1];
16
17 // Base case: there's one way to make amount 0 (use no coins)
18 dp[0][0] = 1;
19
20 // Iterate through each coin type
21 for (int i = 1; i <= numCoins; i++) {
22 // For each possible amount from 0 to target
23 for (int j = 0; j <= targetAmount; j++) {
24 // Option 1: Don't use the current coin
25 // Number of ways remains same as without this coin
26 dp[i][j] = dp[i - 1][j];
27
28 // Option 2: Use the current coin (if amount allows)
29 // Add the number of ways when we use this coin
30 if (j >= coins[i - 1]) {
31 dp[i][j] += dp[i][j - coins[i - 1]];
32 }
33 }
34 }
35
36 // Return the number of ways to make the target amount using all coins
37 return dp[numCoins][targetAmount];
38 }
39}
40
1class Solution {
2public:
3 int change(int amount, vector<int>& coins) {
4 int numCoins = coins.size();
5 int targetAmount = amount;
6
7 // dp[i][j] represents the number of ways to make amount j using first i coins
8 // Using unsigned to handle potential overflow in intermediate calculations
9 unsigned dp[numCoins + 1][targetAmount + 1];
10
11 // Initialize all values to 0
12 memset(dp, 0, sizeof(dp));
13
14 // Base case: there's one way to make amount 0 (use no coins)
15 dp[0][0] = 1;
16
17 // Fill the dp table
18 for (int i = 1; i <= numCoins; ++i) {
19 for (int j = 0; j <= targetAmount; ++j) {
20 // Option 1: Don't use the current coin
21 // Number of ways equals to using first (i-1) coins to make amount j
22 dp[i][j] = dp[i - 1][j];
23
24 // Option 2: Use the current coin (if possible)
25 // We can use coin[i-1] if the remaining amount is non-negative
26 if (j >= coins[i - 1]) {
27 // Add the number of ways to make amount (j - coins[i-1])
28 // using first i coins (coin can be reused)
29 dp[i][j] += dp[i][j - coins[i - 1]];
30 }
31 }
32 }
33
34 // Return the number of ways to make the target amount using all coins
35 return dp[numCoins][targetAmount];
36 }
37};
38
1/**
2 * Calculates the number of ways to make change for a given amount using the provided coins.
3 * This is a classic dynamic programming solution to the coin change problem.
4 *
5 * @param amount - The target amount to make change for
6 * @param coins - Array of coin denominations available
7 * @returns The number of distinct ways to make change for the amount
8 */
9function change(amount: number, coins: number[]): number {
10 const numCoins: number = coins.length;
11 const targetAmount: number = amount;
12
13 // Create a 2D DP table where:
14 // - rows represent using first i coin types (0 to numCoins)
15 // - columns represent amounts from 0 to targetAmount
16 // - dp[i][j] = number of ways to make amount j using first i coin types
17 const dp: number[][] = Array.from(
18 { length: numCoins + 1 },
19 () => Array(targetAmount + 1).fill(0)
20 );
21
22 // Base case: there's one way to make amount 0 (use no coins)
23 dp[0][0] = 1;
24
25 // Fill the DP table
26 for (let coinIndex = 1; coinIndex <= numCoins; coinIndex++) {
27 for (let currentAmount = 0; currentAmount <= targetAmount; currentAmount++) {
28 // Option 1: Don't use the current coin type
29 // Inherit the number of ways from previous coin types
30 dp[coinIndex][currentAmount] = dp[coinIndex - 1][currentAmount];
31
32 // Option 2: Use the current coin type (if possible)
33 // Add the number of ways when we include this coin
34 const currentCoinValue: number = coins[coinIndex - 1];
35 if (currentAmount >= currentCoinValue) {
36 dp[coinIndex][currentAmount] += dp[coinIndex][currentAmount - currentCoinValue];
37 }
38 }
39 }
40
41 // Return the number of ways to make the target amount using all available coins
42 return dp[numCoins][targetAmount];
43}
44
Time and Space Complexity
The time complexity of this dynamic programming solution is O(m × n)
, where m
is the number of types of coins and n
is the target amount. This is because we have two nested loops: the outer loop iterates through all m
coins, and the inner loop iterates through all possible amounts from 0
to n
. Each cell f[i][j]
is computed exactly once in constant time.
The space complexity is O(m × n)
. We create a 2D array f
with dimensions (m + 1) × (n + 1)
to store the number of ways to make each amount using the first i
types of coins. Each cell stores a single integer value, resulting in total space usage proportional to m × n
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Confusing Combinations with Permutations
The Pitfall: A common mistake is treating this as a permutation problem, where order matters. For instance, counting {1, 2, 2} and {2, 1, 2} as different ways to make 5.
Why it happens: The instinct might be to iterate through amounts first, then coins:
# WRONG approach - counts permutations
for j in range(1, amount + 1):
for coin in coins:
if j >= coin:
dp[j] += dp[j - coin]
The Solution: Always iterate through coins in the outer loop and amounts in the inner loop. This ensures each combination is counted exactly once:
# CORRECT approach - counts combinations
for coin in coins:
for j in range(coin, amount + 1):
dp[j] += dp[j - coin]
2. Incorrect Base Case Initialization
The Pitfall: Forgetting to set dp[0][0] = 1
or incorrectly initializing the entire first row/column.
Why it happens: It's not immediately intuitive that there's exactly one way to make amount 0 (by selecting no coins).
The Solution: Only dp[0][0]
should be 1. All other dp[0][j]
for j > 0
should remain 0 (there's no way to make a positive amount with zero coins).
3. Space Optimization Without Understanding the Dependencies
The Pitfall: Attempting to optimize from 2D to 1D array incorrectly:
# WRONG - modifying values we still need to read
dp = [0] * (amount + 1)
dp[0] = 1
for coin in coins:
for j in range(amount + 1): # Wrong direction!
if j >= coin:
dp[j] += dp[j - coin]
The Solution: When optimizing to 1D, iterate through amounts from left to right (not right to left like in 0/1 knapsack):
# CORRECT 1D optimization
dp = [0] * (amount + 1)
dp[0] = 1
for coin in coins:
for j in range(coin, amount + 1): # Start from coin value
dp[j] += dp[j - coin]
4. Index Confusion in 2D Implementation
The Pitfall: Mixing up the coin index with the actual coin value:
# WRONG - using i instead of coins[i-1]
for i in range(1, num_coins + 1):
for j in range(amount + 1):
dp[i][j] = dp[i-1][j]
if j >= i: # Wrong! Should be j >= coins[i-1]
dp[i][j] += dp[i][j - i]
The Solution: Always extract the coin value explicitly to avoid confusion:
for i in range(1, num_coins + 1):
coin_value = coins[i - 1] # Clear variable name
for j in range(amount + 1):
dp[i][j] = dp[i-1][j]
if j >= coin_value:
dp[i][j] += dp[i][j - coin_value]
Which of these properties could exist for a graph but not a tree?
Recommended Readings
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!