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 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" - input: "coins = [2], amount = 3" output: "-1" explanation: "Cannot make amount 3 with only coin 2." - input: "coins = [1], amount = 0" output: "0" explanation: "Amount 0 needs 0 coins." explanation: approach: | 1. Create a DP array where dp[i] = min coins needed for amount i 2. Initialize dp[0] = 0 (zero coins for zero amount) 3. For each amount from 1 to target, try each coin 4. If coin <= current amount, dp[i] = min(dp[i], dp[i - coin] + 1) 5. Return dp[amount] if valid, else -1 intuition: | This is the classic unbounded knapsack problem. For each amount, we ask: "What's the minimum coins needed if I use coin c as the last coin?" If we use coin c last, we need 1 + dp[amount - c] coins. We try all possible "last coins" and take the minimum. This optimal substructure makes it perfect for DP. common_pitfalls: - title: Wrong initialization description: | Initialize dp array to infinity (or amount + 1), not 0. dp[0] = 0 is the only base case. wrong_approach: "Initializing all dp values to 0" correct_approach: "dp = [float('inf')] * (amount + 1); dp[0] = 0" - title: Not checking if subproblem is solvable description: | Before using dp[i - coin], ensure i >= coin and dp[i - coin] is valid. - title: Returning wrong value for impossible case description: | If dp[amount] is still infinity, return -1, not infinity. key_takeaways: - Classic unbounded knapsack problem - Bottom-up DP builds solution from smaller amounts - Try each coin as the "last coin" for each amount - Greedy doesn't work here (counterexample: coins=[1,3,4], amount=6) time_complexity: "O(amount × coins)" space_complexity: "O(amount)" complexity_explanation: | Time: For each amount (1 to target), we try each coin. Space: DP array of size amount + 1. solutions: - approach_name: Bottom-Up DP (Optimal) is_optimal: true code: | def coin_change(coins: list[int], amount: int) -> int: dp = [float('inf')] * (amount + 1) dp[0] = 0 for i in range(1, amount + 1): for coin in coins: if coin <= i and dp[i - coin] != float('inf'): dp[i] = min(dp[i], dp[i - coin] + 1) return dp[amount] if dp[amount] != float('inf') else -1 explanation: | Build up from amount 0. For each amount, try using each coin as the last coin. Take the minimum of all valid options. - 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 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 if next_sum == amount: return num_coins + 1 if next_sum < amount and next_sum not in visited: visited.add(next_sum) queue.append((next_sum, num_coins + 1)) return -1 explanation: | BFS finds shortest path in unweighted graph. First time we reach 'amount' is the minimum coins. Less space-efficient than DP for this problem.