questions C

This commit is contained in:
2025-05-25 10:16:13 +01:00
parent 1e0aebfbfd
commit e6a22f98f8
85 changed files with 16925 additions and 0 deletions

View 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)
&nbsp;
**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
&nbsp;
**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"
&nbsp;
**Step 4: Return the result**
- Return `dp[amount]`, which contains the total number of combinations
&nbsp;
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.