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.
