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

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

    &nbsp;

    **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

    &nbsp;

    **Step 3: Return the answer**

    - If `dp[amount]` is still infinity, return `-1` (impossible)
    - Otherwise, return `dp[amount]`

    &nbsp;

    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.
