title: Coin Change II slug: coin-change-ii difficulty: medium leetcode_id: 518 leetcode_url: https://leetcode.com/problems/coin-change-ii/ categories: - arrays - dynamic-programming patterns: - dynamic-programming description: | You are given an integer array `coins` representing coins of different denominations and an integer `amount` representing a total amount of money. Return *the number of combinations that make up that amount*. If that amount of money cannot be made up by any combination of the coins, return `0`. You may assume that you have an infinite number of each kind of coin. The answer is **guaranteed** to fit into a signed **32-bit** integer. constraints: | - `1 <= coins.length <= 300` - `1 <= coins[i] <= 5000` - All the values of `coins` are **unique** - `0 <= amount <= 5000` examples: - input: "amount = 5, coins = [1,2,5]" output: "4" explanation: "There are four ways to make up the amount: 5=5, 5=2+2+1, 5=2+1+1+1, 5=1+1+1+1+1" - input: "amount = 3, coins = [2]" output: "0" explanation: "The amount of 3 cannot be made up just with coins of 2." - input: "amount = 10, coins = [10]" output: "1" explanation: "There is only one way: use a single coin of denomination 10." explanation: intuition: | Imagine you're a cashier with unlimited coins of certain denominations, and you need to count how many *distinct* ways you can give change for a specific amount. The key insight is understanding the difference between **combinations** and **permutations**. If you have coins `[1, 2]` and need to make amount `3`, the combinations `1+2` and `2+1` are the **same** — they both use one coin of each type. We only count this once. Think of it like filling a shopping bag: the order you put items in doesn't matter, only *what* items end up in the bag. To avoid counting the same combination multiple times, we process **one coin type at a time**. For each coin, we ask: "How many ways can I make each amount using this coin (zero or more times) plus coins I've already considered?" This is the classic **Unbounded Knapsack** pattern where each item (coin) can be used unlimited times, and we're counting combinations, not finding a minimum. approach: | We solve this using **1D Dynamic Programming** with a space-optimised approach: **Step 1: Define the DP array** - `dp[i]`: The number of ways to make amount `i` using the coins considered so far - Size: `amount + 1` (to include amount `0` through `amount`) - Initial value: `dp[0] = 1` (there's exactly one way to make amount `0`: use no coins)   **Step 2: Process coins one by one (outer loop)** - Iterate through each coin in `coins` - This ensures we count **combinations**, not permutations - By fixing the coin order, `[1, 2]` and `[2, 1]` won't be counted separately   **Step 3: Update amounts that can use this coin (inner loop)** - For each coin, iterate through amounts from `coin` to `amount` - For each amount `a`, add `dp[a - coin]` to `dp[a]` - This represents: "ways to make amount `a` by using at least one of this coin"   **Step 4: Return the result** - Return `dp[amount]`, which contains the total number of combinations   The key to avoiding duplicate counting is the loop order: coins in the outer loop, amounts in the inner loop. This ensures each combination is counted exactly once. common_pitfalls: - title: Counting Permutations Instead of Combinations description: | If you swap the loop order (amounts in outer, coins in inner), you'll count **permutations** instead of combinations. For example, with `coins = [1, 2]` and `amount = 3`: - Correct (combinations): `[1,1,1], [1,2]` → 2 ways - Wrong (permutations): `[1,1,1], [1,2], [2,1]` → 3 ways The `[1,2]` and `[2,1]` are the same combination but different permutations. Processing coins in the outer loop ensures each coin type is considered in a fixed order. wrong_approach: "Outer loop over amounts, inner loop over coins" correct_approach: "Outer loop over coins, inner loop over amounts" - title: Wrong Base Case description: | Forgetting to initialise `dp[0] = 1` is a common mistake. There is exactly **one way** to make amount `0`: use no coins at all. If you initialise `dp[0] = 0`, all subsequent values remain `0` since there's no base case to build from. wrong_approach: "dp[0] = 0 or leaving it uninitialised" correct_approach: "dp[0] = 1 (one way to make zero: use nothing)" - title: Inner Loop Starting Point description: | The inner loop must start from `coin`, not from `0` or `1`. - Starting from `0` would try to access `dp[negative]` - Starting from `1` would miss the case where `amount == coin` We can only add a coin to amounts >= its value. wrong_approach: "for a in range(amount + 1)" correct_approach: "for a in range(coin, amount + 1)" - title: Confusing with Coin Change I description: | Coin Change I asks for the **minimum number** of coins, while Coin Change II asks for the **number of combinations**. They require different DP formulations: - Coin Change I: `dp[a] = min(dp[a], dp[a - coin] + 1)` - Coin Change II: `dp[a] = dp[a] + dp[a - coin]` Using `min` here would give incorrect results. key_takeaways: - "**Unbounded Knapsack pattern**: When items can be reused unlimited times and order doesn't matter, use the 'coin outer, amount inner' loop structure" - "**Combinations vs Permutations**: Loop order determines whether you count ordered or unordered selections — this is a critical distinction in DP problems" - "**Space optimisation**: 2D DP can often be reduced to 1D when each row only depends on the previous row (or itself)" - "**Related problems**: This extends to Coin Change I (minimum coins), Combination Sum IV (permutations), and knapsack variants" time_complexity: "O(n × amount). We iterate through each of the `n` coins, and for each coin, we iterate through amounts from `coin` to `amount`." space_complexity: "O(amount). We use a 1D array of size `amount + 1` to store the number of combinations for each amount." solutions: - approach_name: 1D Dynamic Programming is_optimal: true code: | def change(amount: int, coins: list[int]) -> int: # dp[i] = number of ways to make amount i dp = [0] * (amount + 1) # Base case: one way to make amount 0 (use no coins) dp[0] = 1 # Process each coin type one at a time # This ensures we count combinations, not permutations for coin in coins: # For each amount that can use this coin for a in range(coin, amount + 1): # Add ways to make (a - coin) using coins considered so far dp[a] += dp[a - coin] return dp[amount] explanation: | **Time Complexity:** O(n × amount) — Nested loops over coins and amounts. **Space Complexity:** O(amount) — Single array of size `amount + 1`. By processing coins in the outer loop, we ensure each coin type is added in a fixed order, avoiding duplicate combinations. The inner loop accumulates ways to use the current coin (zero or more times) with previously processed coins. - approach_name: 2D Dynamic Programming is_optimal: false code: | def change(amount: int, coins: list[int]) -> int: n = len(coins) # dp[i][a] = ways to make amount a using first i coin types dp = [[0] * (amount + 1) for _ in range(n + 1)] # Base case: one way to make amount 0 with any set of coins for i in range(n + 1): dp[i][0] = 1 for i in range(1, n + 1): coin = coins[i - 1] for a in range(amount + 1): # Don't use this coin type dp[i][a] = dp[i - 1][a] # Use this coin type (if possible) if a >= coin: dp[i][a] += dp[i][a - coin] return dp[n][amount] explanation: | **Time Complexity:** O(n × amount) — Same as 1D approach. **Space Complexity:** O(n × amount) — 2D array storing states for each coin prefix. This explicit 2D formulation makes the state transition clearer: `dp[i][a]` represents ways to make amount `a` using only the first `i` coin types. The 1D solution is a space optimisation of this, possible because row `i` only depends on row `i` and row `i-1`. - approach_name: Recursive with Memoization is_optimal: false code: | def change(amount: int, coins: list[int]) -> int: from functools import lru_cache @lru_cache(maxsize=None) def count_ways(coin_index: int, remaining: int) -> int: # Base case: exact amount achieved if remaining == 0: return 1 # Base case: no more coins or overshot if coin_index >= len(coins) or remaining < 0: return 0 coin = coins[coin_index] # Choice 1: Skip this coin type entirely skip = count_ways(coin_index + 1, remaining) # Choice 2: Use at least one of this coin (can use more) use = count_ways(coin_index, remaining - coin) return skip + use return count_ways(0, amount) explanation: | **Time Complexity:** O(n × amount) — Each unique state `(coin_index, remaining)` is computed once. **Space Complexity:** O(n × amount) — Memoization cache plus recursion stack. This top-down approach makes the decision tree explicit: at each coin, we either skip it entirely or use at least one. The `coin_index` parameter ensures we process coins in order, counting combinations rather than permutations. Less efficient than iterative DP due to function call overhead.