title: Coin Change slug: coin-change difficulty: medium leetcode_id: 322 leetcode_url: https://leetcode.com/problems/coin-change/ categories: - dynamic-programming - arrays patterns: - dynamic-programming function_signature: "def coin_change(coins: list[int], amount: int) -> int:" test_cases: visible: - input: { coins: [1, 2, 5], amount: 11 } expected: 3 - input: { coins: [2], amount: 3 } expected: -1 - input: { coins: [1], amount: 0 } expected: 0 hidden: - input: { coins: [1], amount: 1 } expected: 1 - input: { coins: [1, 3, 4], amount: 6 } expected: 2 - input: { coins: [2, 5, 10], amount: 7 } expected: 2 - input: { coins: [186, 419, 83, 408], amount: 6249 } expected: 20 - input: { coins: [1, 2, 5], amount: 100 } expected: 20 - input: { coins: [3, 7], amount: 5 } expected: -1 - input: { coins: [1, 5, 10, 25], amount: 30 } expected: 2 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 fewest number of coins that you need to make up that amount*. If that amount of money cannot be made up by any combination of the coins, return `-1`. You may assume that you have an **infinite number** of each kind of coin. constraints: | - `1 <= coins.length <= 12` - `1 <= coins[i] <= 2^31 - 1` - `0 <= amount <= 10^4` examples: - input: "coins = [1,2,5], amount = 11" output: "3" explanation: "11 = 5 + 5 + 1, using 3 coins." - input: "coins = [2], amount = 3" output: "-1" explanation: "Cannot make amount 3 with only coin denomination 2." - input: "coins = [1], amount = 0" output: "0" explanation: "Amount 0 requires 0 coins." explanation: intuition: | Imagine you're at a vending machine that gives change. You want to give the customer exactly 11 cents using the fewest coins possible from denominations [1, 2, 5]. How do you think about this? Think of it like this: if I knew the minimum coins needed for amounts 0 through 10, I could figure out amount 11 by asking: "What if I use a 1-cent coin last? A 2-cent? A 5-cent?" - Using 1-cent last: I need `coins(10) + 1` - Using 2-cent last: I need `coins(9) + 1` - Using 5-cent last: I need `coins(6) + 1` The answer is the **minimum** of these options. This is the **optimal substructure** that makes dynamic programming work. This is the classic **unbounded knapsack** pattern — "unbounded" because we can use each coin infinitely many times. approach: | We solve this using **Bottom-Up Dynamic Programming**: **Step 1: Create and initialise the DP array** - Create `dp` of size `amount + 1`, where `dp[i]` = minimum coins for amount `i` - Initialise all values to infinity (or `amount + 1`) — meaning "impossible so far" - Set `dp[0] = 0` as the base case: zero coins needed for amount zero   **Step 2: Build up solutions for each amount** - For each amount `i` from 1 to `amount`: - Try each coin denomination - If `coin <= i` (coin fits), check if using this coin improves our answer: - `dp[i] = min(dp[i], dp[i - coin] + 1)` - The `+1` accounts for using this coin; `dp[i - coin]` is the subproblem   **Step 3: Return the answer** - If `dp[amount]` is still infinity, return `-1` (impossible) - Otherwise, return `dp[amount]`   This builds solutions from smaller amounts to larger ones, ensuring we always have the subproblem solutions ready when we need them. common_pitfalls: - title: Wrong Initialisation description: | Initialising the DP array to `0` instead of infinity is a critical error. If `dp[5] = 0`, we'd incorrectly think amount 5 needs 0 coins! We use `float('inf')` (or `amount + 1` as a practical upper bound) to represent "not yet achievable". Only `dp[0] = 0` should start as zero. wrong_approach: "dp = [0] * (amount + 1)" correct_approach: "dp = [float('inf')] * (amount + 1); dp[0] = 0" - title: Greedy Doesn't Work Here description: | It's tempting to always use the largest coin first (greedy). But this fails! **Counterexample**: `coins = [1, 3, 4]`, `amount = 6` - Greedy: `4 + 1 + 1 = 6` (3 coins) - Optimal: `3 + 3 = 6` (2 coins) The greedy choice can block us from finding the actual minimum. wrong_approach: "Always pick the largest coin that fits" correct_approach: "Try all coins and take the minimum via DP" - title: Not Checking for Impossible Cases description: | If `dp[amount]` is still infinity after filling the table, no valid combination exists. Return `-1`, not infinity or some arbitrary value. This happens when no coin divides evenly into the required amounts (e.g., `coins = [2]`, `amount = 3`). wrong_approach: "return dp[amount]" correct_approach: "return dp[amount] if dp[amount] != float('inf') else -1" key_takeaways: - "**Unbounded knapsack pattern**: Each item (coin) can be used unlimited times — different from 0/1 knapsack" - "**Greedy fails for coin change**: Classic counterexample shows why DP is necessary" - "**Bottom-up builds confidence**: Solving smaller amounts first guarantees subproblems are ready" - "**Foundation for variations**: This extends to counting combinations, finding exact change with fewest bills, etc." time_complexity: "O(amount × n). For each amount from 1 to target, we try each of the n coins." space_complexity: "O(amount). The DP array stores one value per amount from 0 to target." solutions: - approach_name: Bottom-Up DP is_optimal: true code: | def coin_change(coins: list[int], amount: int) -> int: # dp[i] = minimum coins needed for amount i # Initialise to "impossible" (any value > amount works) dp = [float('inf')] * (amount + 1) # Base case: 0 coins needed for amount 0 dp[0] = 0 # Build solutions from amount 1 to target for i in range(1, amount + 1): # Try each coin as the "last coin" used for coin in coins: # Can only use this coin if it fits and subproblem is solvable if coin <= i and dp[i - coin] != float('inf'): dp[i] = min(dp[i], dp[i - coin] + 1) # Return result, or -1 if impossible return dp[amount] if dp[amount] != float('inf') else -1 explanation: | **Time Complexity:** O(amount × n) — For each of `amount` values, we check n coins. **Space Complexity:** O(amount) — DP array of size `amount + 1`. We build up from amount 0. For each amount, we try using each coin as the last coin and take the minimum. The key insight: if we know the minimum coins for smaller amounts, we can compute larger amounts by adding one coin at a time. - approach_name: BFS (Alternative) is_optimal: false code: | from collections import deque def coin_change(coins: list[int], amount: int) -> int: if amount == 0: return 0 # BFS: each "level" represents using one more coin visited = {0} queue = deque([(0, 0)]) # (current_sum, num_coins) while queue: current, num_coins = queue.popleft() for coin in coins: next_sum = current + coin # Found exact amount if next_sum == amount: return num_coins + 1 # Valid state we haven't seen if next_sum < amount and next_sum not in visited: visited.add(next_sum) queue.append((next_sum, num_coins + 1)) # No valid combination found return -1 explanation: | **Time Complexity:** O(amount × n) — Similar to DP in the worst case. **Space Complexity:** O(amount) — Visited set can hold up to `amount` states. BFS naturally finds the shortest path (minimum coins) in an unweighted graph. Each state is a sum, and edges represent adding a coin. The first time we reach `amount`, we've found the minimum. Generally less efficient than DP for this problem due to queue overhead.