title: Combination Sum II
slug: combination-sum-ii
difficulty: medium
leetcode_id: 40
leetcode_url: https://leetcode.com/problems/combination-sum-ii/
categories:
  - arrays
  - recursion
patterns:
  - backtracking

description: |
  Given a collection of candidate numbers (`candidates`) and a target number (`target`), find all unique combinations in `candidates` where the candidate numbers sum to `target`.

  Each number in `candidates` may only be used **once** in the combination.

  **Note:** The solution set must not contain duplicate combinations.

constraints: |
  - `1 <= candidates.length <= 100`
  - `1 <= candidates[i] <= 50`
  - `1 <= target <= 30`

examples:
  - input: "candidates = [10,1,2,7,6,1,5], target = 8"
    output: "[[1,1,6],[1,2,5],[1,7],[2,6]]"
    explanation: "The unique combinations that sum to 8 are: [1,1,6], [1,2,5], [1,7], and [2,6]. Note that [1,1,6] uses both 1s from the input."
  - input: "candidates = [2,5,2,1,2], target = 5"
    output: "[[1,2,2],[5]]"
    explanation: "Two unique combinations sum to 5: [1,2,2] and [5]."

explanation:
  intuition: |
    Imagine you have a bag of coins where some coins have the same value. You need to find all ways to make exact change for a target amount, but once you use a coin, it's gone — you can't use the same physical coin twice.

    The challenge compared to Combination Sum I is twofold:
    1. **No reuse**: Each element can only be used once, so after picking an element, we must move to the next index
    2. **Duplicates in input**: The input may contain duplicate values like `[1,1,2]`, and we need to avoid generating duplicate combinations

    Think of it like this: if you have two coins both worth 1, picking the first coin vs. the second coin leads to the same combination `[1,...]`. To avoid counting this twice, we need a way to **skip duplicate values at the same decision level**.

    The key insight is that after sorting, duplicates are adjacent. At each decision point in our backtracking tree, if we've already explored a branch starting with a particular value, we should skip all other occurrences of that same value at that level. However, we *can* use duplicate values in the same combination — we just can't start multiple branches with the same value at the same tree level.

  approach: |
    We solve this using **Backtracking with Duplicate Skipping**:

    **Step 1: Sort the candidates**

    - Sorting brings duplicate values together, making them easy to detect and skip
    - It also enables pruning: once a candidate exceeds the remaining target, we can stop

    &nbsp;

    **Step 2: Define the recursive backtrack function**

    - `backtrack(start, remaining, current_combination)`
    - `start`: index of the first candidate we can consider (elements before are already "used" or skipped)
    - `remaining`: how much more we need to reach the target
    - `current_combination`: the combination being built

    &nbsp;

    **Step 3: Base case — target reached**

    - If `remaining == 0`, we've found a valid combination
    - Add a **copy** of `current_combination` to results
    - Return to explore other paths

    &nbsp;

    **Step 4: Recursive case with duplicate skipping**

    - Loop through candidates starting from index `start`
    - For each candidate at index `i`:
      - **Skip duplicates at same level**: If `i > start` and `candidates[i] == candidates[i-1]`, skip this candidate
      - **Prune**: If `candidates[i] > remaining`, break (no point trying larger candidates)
      - **Choose**: Add `candidates[i]` to `current_combination`
      - **Explore**: Recursively call `backtrack(i + 1, remaining - candidates[i], current_combination)`
        - Note: we pass `i + 1`, not `i`, because each element can only be used once
      - **Unchoose**: Remove the last element (backtrack)

    &nbsp;

    **Step 5: Return all valid combinations**

    - Call `backtrack(0, target, [])` to start the search
    - Return the collected results

  common_pitfalls:
    - title: Generating Duplicate Combinations
      description: |
        Without proper duplicate handling, an input like `[1,1,2]` with target 3 would generate `[1,2]` twice — once using the first 1, once using the second 1.

        The fix requires understanding *where* to skip: we only skip duplicates **at the same decision level**. The condition `i > start and candidates[i] == candidates[i-1]` ensures we skip duplicates only when they would start a new branch at the same level, not when they're used deeper in the same path.

        For example, with sorted `[1,1,2]` and target 3:
        - First branch: pick index 0 (value 1), then can pick index 1 (value 1) → `[1,1,...]`
        - At the top level, skip index 1 (value 1) because we already tried 1 at this level
      wrong_approach: "No duplicate checking, or skipping all duplicates everywhere"
      correct_approach: "Skip duplicates only at the same tree level using i > start check"

    - title: Reusing Elements (Wrong Recursion Index)
      description: |
        In Combination Sum I, we recurse with index `i` to allow reuse. Here, each element can only be used once, so we must recurse with `i + 1`.

        If you accidentally pass `i` instead of `i + 1`, you'll generate invalid combinations that use the same element multiple times, like `[2,2,2]` from an input that only contains one 2.
      wrong_approach: "backtrack(i, ...) after choosing candidates[i]"
      correct_approach: "backtrack(i + 1, ...) to move past the used element"

    - title: Incorrect Duplicate Skip Condition
      description: |
        A common mistake is using just `candidates[i] == candidates[i-1]` without the `i > start` check. This would incorrectly skip valid uses of duplicates within the same combination.

        For example, with `[1,1,6]` and target 8, we want to allow the combination `[1,1,6]`. The second 1 is at index 1, and when we're exploring from start=1 (after picking the first 1), we have `i == start`, so we *don't* skip — correctly allowing `[1,1,6]`.
      wrong_approach: "if candidates[i] == candidates[i-1]: continue"
      correct_approach: "if i > start and candidates[i] == candidates[i-1]: continue"

    - title: Forgetting to Sort
      description: |
        The duplicate skipping logic relies on duplicates being adjacent. Without sorting, duplicate values could be scattered throughout the array, and the `candidates[i] == candidates[i-1]` check would miss them.

        Always sort the candidates array first before starting the backtracking.
      wrong_approach: "Attempting duplicate detection without sorting"
      correct_approach: "Sort candidates first, then use adjacent comparison"

  key_takeaways:
    - "**Duplicate skipping at same level**: The condition `i > start and candidates[i] == candidates[i-1]` is the key — it prevents duplicate branches at the same decision level while allowing duplicates within a combination"
    - "**Use once vs. reuse**: The critical difference from Combination Sum I is recursing with `i + 1` instead of `i` to prevent element reuse"
    - "**Sorting enables everything**: Sorting makes duplicates adjacent for detection and enables early termination pruning"
    - "**Common pattern**: This duplicate-skipping technique appears in many backtracking problems (Subsets II, Permutations II) — master it once, apply it everywhere"

  time_complexity: "O(2^n). In the worst case, we might explore all possible subsets of the candidates array. The actual complexity is often better due to pruning and the target constraint limiting combination lengths."
  space_complexity: "O(n). The recursion depth is at most n (the number of candidates), and we use O(n) space for the current combination being built."

solutions:
  - approach_name: Backtracking with Duplicate Skipping
    is_optimal: true
    code: |
      def combination_sum2(candidates: list[int], target: int) -> list[list[int]]:
          result = []
          # Sort to group duplicates together and enable pruning
          candidates.sort()

          def backtrack(start: int, remaining: int, current: list[int]) -> None:
              # Base case: found a valid combination
              if remaining == 0:
                  result.append(current[:])  # Append a copy
                  return

              for i in range(start, len(candidates)):
                  candidate = candidates[i]

                  # Skip duplicates at the same decision level
                  # i > start ensures we don't skip the first occurrence at this level
                  if i > start and candidate == candidates[i - 1]:
                      continue

                  # Pruning: if this candidate exceeds remaining, all after will too
                  if candidate > remaining:
                      break

                  # Choose: add this candidate
                  current.append(candidate)
                  # Explore: move to next index (each element used once)
                  backtrack(i + 1, remaining - candidate, current)
                  # Unchoose: remove the candidate (backtrack)
                  current.pop()

          backtrack(0, target, [])
          return result
    explanation: |
      **Time Complexity:** O(2^n) — In the worst case, we explore all subsets. Pruning typically reduces this significantly.

      **Space Complexity:** O(n) — Recursion depth is bounded by the number of candidates.

      The key insight is the duplicate-skipping condition `i > start and candidate == candidates[i-1]`. This skips duplicate values only at the same tree level, preventing duplicate combinations while still allowing the same value to appear multiple times in a single combination (using different elements from the input).

  - approach_name: Backtracking with Counter
    is_optimal: false
    code: |
      from collections import Counter

      def combination_sum2(candidates: list[int], target: int) -> list[list[int]]:
          result = []
          # Count occurrences of each candidate
          counter = Counter(candidates)
          # Get unique candidates sorted
          unique_candidates = sorted(counter.keys())

          def backtrack(index: int, remaining: int, current: list[int]) -> None:
              # Base case: found a valid combination
              if remaining == 0:
                  result.append(current[:])
                  return

              for i in range(index, len(unique_candidates)):
                  candidate = unique_candidates[i]

                  # Pruning: if this candidate exceeds remaining, stop
                  if candidate > remaining:
                      break

                  # Skip if we've used all occurrences of this candidate
                  if counter[candidate] == 0:
                      continue

                  # Choose: use one occurrence of this candidate
                  current.append(candidate)
                  counter[candidate] -= 1

                  # Explore: can reuse same index (might have more of this value)
                  backtrack(i, remaining - candidate, current)

                  # Unchoose: restore the count
                  current.pop()
                  counter[candidate] += 1

          backtrack(0, target, [])
          return result
    explanation: |
      **Time Complexity:** O(2^n) — Same worst-case as the standard approach.

      **Space Complexity:** O(n) — For the counter and recursion stack.

      This alternative approach uses a Counter to track how many times each unique value can still be used. Instead of skipping duplicates with index comparisons, we naturally handle them by decrementing and checking counts. We stay at the same index (pass `i`) because we might use the same value multiple times if its count allows. This is conceptually cleaner but uses slightly more memory for the counter.

  - approach_name: Iterative with Bitmask
    is_optimal: false
    code: |
      def combination_sum2(candidates: list[int], target: int) -> list[list[int]]:
          candidates.sort()
          n = len(candidates)
          result = []
          seen = set()

          # Iterate through all possible subsets using bitmask
          for mask in range(1 << n):
              subset = []
              total = 0

              for i in range(n):
                  if mask & (1 << i):
                      subset.append(candidates[i])
                      total += candidates[i]

              # Check if this subset sums to target
              if total == target:
                  # Convert to tuple for deduplication
                  key = tuple(subset)
                  if key not in seen:
                      seen.add(key)
                      result.append(subset)

          return result
    explanation: |
      **Time Complexity:** O(2^n * n) — We enumerate all 2^n subsets and process each in O(n) time.

      **Space Complexity:** O(2^n) — In the worst case, we might store many subsets in the seen set.

      This brute-force approach generates all possible subsets using bit manipulation, filters those that sum to target, and uses a set to eliminate duplicates. While correct, it's inefficient because it doesn't prune impossible paths early and requires extra memory for deduplication. It's included to illustrate why backtracking with pruning is preferred.
