246 lines
11 KiB
YAML
246 lines
11 KiB
YAML
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
|
||
|
||
function_signature: "def change(amount: int, coins: list[int]) -> int:"
|
||
|
||
test_cases:
|
||
visible:
|
||
- input: { amount: 5, coins: [1, 2, 5] }
|
||
expected: 4
|
||
- input: { amount: 3, coins: [2] }
|
||
expected: 0
|
||
- input: { amount: 10, coins: [10] }
|
||
expected: 1
|
||
hidden:
|
||
- input: { amount: 0, coins: [1, 2, 5] }
|
||
expected: 1
|
||
- input: { amount: 1, coins: [1] }
|
||
expected: 1
|
||
- input: { amount: 100, coins: [1, 5, 10, 25] }
|
||
expected: 242
|
||
- input: { amount: 7, coins: [2, 3, 5] }
|
||
expected: 2
|
||
- input: { amount: 500, coins: [1, 2, 5] }
|
||
expected: 12701
|
||
- input: { amount: 4, coins: [1, 2, 3] }
|
||
expected: 4
|
||
|
||
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.
|