questions C
This commit is contained in:
221
backend/data/questions/coin-change-ii.yaml
Normal file
221
backend/data/questions/coin-change-ii.yaml
Normal file
@@ -0,0 +1,221 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user