title: Subsets
slug: subsets
difficulty: medium
leetcode_id: 78
leetcode_url: https://leetcode.com/problems/subsets/
categories:
  - arrays
  - recursion
patterns:
  - backtracking

function_signature: "def subsets(nums: list[int]) -> list[list[int]]:"

test_cases:
  visible:
    - input: { nums: [1, 2, 3] }
      expected: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
    - input: { nums: [0] }
      expected: [[], [0]]
    - input: { nums: [1, 2] }
      expected: [[], [1], [2], [1, 2]]
  hidden:
    - input: { nums: [] }
      expected: [[]]
    - input: { nums: [5, 10, 15] }
      expected: [[], [5], [10], [5, 10], [15], [5, 15], [10, 15], [5, 10, 15]]
    - input: { nums: [-1, 0, 1] }
      expected: [[], [-1], [0], [-1, 0], [1], [-1, 1], [0, 1], [-1, 0, 1]]
    - input: { nums: [1, 2, 3, 4] }
      expected: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3], [4], [1, 4], [2, 4], [1, 2, 4], [3, 4], [1, 3, 4], [2, 3, 4], [1, 2, 3, 4]]
    - input: { nums: [9] }
      expected: [[], [9]]

description: |
  Given an integer array `nums` of **unique** elements, return *all possible subsets* (the power set).

  The solution set **must not** contain duplicate subsets. Return the solution in **any order**.

constraints: |
  - `1 <= nums.length <= 10`
  - `-10 <= nums[i] <= 10`
  - All the numbers of `nums` are **unique**

examples:
  - input: "nums = [1,2,3]"
    output: "[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]"
    explanation: "The power set contains all 2^3 = 8 subsets, from the empty set to the full set."
  - input: "nums = [0]"
    output: "[[],[0]]"
    explanation: "With a single element, we have two subsets: the empty set and the set containing just that element."

explanation:
  intuition: |
    Think of building subsets as a series of **binary decisions**. For each element in the array, you have exactly two choices: either **include it** in the current subset or **exclude it**.

    Imagine you're packing a bag and laying out items on a table. For each item, you decide "yes, take it" or "no, leave it." If you have 3 items, you make 3 independent yes/no decisions, giving you 2^3 = 8 possible combinations — from taking nothing (empty bag) to taking everything.

    This decision tree structure naturally maps to **backtracking**: start with an empty subset, and at each step, branch into two paths — one where you add the current element, one where you don't. When you've made decisions for all elements, you've formed one complete subset.

    The key insight is that every subset corresponds to a unique path through this decision tree. By exploring all paths systematically, we generate the complete power set.

  approach: |
    We solve this using **Backtracking** to explore all include/exclude decisions:

    **Step 1: Initialise the result and define the recursive function**

    - `result`: An empty list that will collect all subsets
    - `backtrack(index, current)`: A recursive function where `index` is the current position in `nums` and `current` is the subset being built

    &nbsp;

    **Step 2: Define the base case**

    - When `index` equals `len(nums)`, we've made decisions for all elements
    - Add a copy of `current` to `result` (copy is important since we'll modify `current` later)

    &nbsp;

    **Step 3: Explore both choices at each step**

    - **Include the element**: Add `nums[index]` to `current`, recurse with `index + 1`, then remove the element (backtrack)
    - **Exclude the element**: Simply recurse with `index + 1` without adding anything

    &nbsp;

    **Step 4: Start the recursion**

    - Call `backtrack(0, [])` to begin from the first element with an empty subset
    - Return `result` containing all 2^n subsets

    &nbsp;

    This systematic exploration guarantees we visit every possible combination exactly once.

  common_pitfalls:
    - title: Forgetting to Copy the Subset
      description: |
        When adding `current` to the result, you must add a **copy**, not a reference:

        ```python
        result.append(current[:])  # Correct: adds a copy
        result.append(current)     # Wrong: adds a reference
        ```

        If you add the reference, all entries in `result` will point to the same list, which gets modified during backtracking. You'll end up with a result full of empty lists or unexpected values.
      wrong_approach: "Appending the list directly without copying"
      correct_approach: "Use `current[:]` or `list(current)` to create a copy"

    - title: Not Backtracking After Recursion
      description: |
        After recursing with an element included, you must **remove it** before exploring the "exclude" path:

        ```python
        current.append(nums[index])
        backtrack(index + 1, current)
        current.pop()  # Essential: undo the choice
        backtrack(index + 1, current)
        ```

        Without `current.pop()`, the element remains in `current` for subsequent branches, corrupting all future subsets.
      wrong_approach: "Forgetting to pop after the recursive call"
      correct_approach: "Always undo modifications after recursing"

    - title: Generating Duplicates
      description: |
        If you accidentally allow revisiting earlier elements, you'll generate duplicates:

        ```python
        # Wrong: starts from 0 each time
        for i in range(len(nums)):
            backtrack(i + 1, current + [nums[i]])

        # Correct: only consider elements after current index
        backtrack(index + 1, current)
        ```

        The key is to always move **forward** in the array. Once you've decided about `nums[i]`, never reconsider it.
      wrong_approach: "Allowing the recursion to revisit earlier indices"
      correct_approach: "Always pass index + 1 to move forward only"

  key_takeaways:
    - "**Backtracking pattern**: For problems asking for 'all combinations' or 'all subsets', think of a decision tree where each node branches based on include/exclude choices"
    - "**Power set property**: An array of `n` unique elements has exactly `2^n` subsets, since each element is independently included or excluded"
    - "**Foundation for harder problems**: This same backtracking template extends to Subsets II (with duplicates), Combinations, Permutations, and many constraint-satisfaction problems"
    - "**Bit manipulation alternative**: Each subset maps to a binary number from `0` to `2^n - 1`, where bit `i` indicates whether `nums[i]` is included"

  time_complexity: "O(n * 2^n). We generate 2^n subsets, and copying each subset takes O(n) time in the worst case."
  space_complexity: "O(n). The recursion depth is at most `n`, and the `current` list holds at most `n` elements. Note: the output itself requires O(n * 2^n) space, but that's not counted as auxiliary space."

solutions:
  - approach_name: Backtracking
    is_optimal: true
    code: |
      def subsets(nums: list[int]) -> list[list[int]]:
          result = []

          def backtrack(index: int, current: list[int]) -> None:
              # Base case: made decisions for all elements
              if index == len(nums):
                  result.append(current[:])  # Add a copy of current subset
                  return

              # Choice 1: Include nums[index]
              current.append(nums[index])
              backtrack(index + 1, current)
              current.pop()  # Backtrack: undo the choice

              # Choice 2: Exclude nums[index]
              backtrack(index + 1, current)

          backtrack(0, [])
          return result
    explanation: |
      **Time Complexity:** O(n * 2^n) — We generate 2^n subsets, each requiring O(n) to copy.

      **Space Complexity:** O(n) — Recursion depth and current subset storage.

      This approach explicitly models the include/exclude decision tree. At each index, we branch into two recursive calls: one with the element added, one without. The backtracking (pop) ensures we can reuse the same list across branches.

  - approach_name: Iterative (Cascading)
    is_optimal: true
    code: |
      def subsets(nums: list[int]) -> list[list[int]]:
          result = [[]]  # Start with empty subset

          for num in nums:
              # For each existing subset, create a new one with num added
              result += [subset + [num] for subset in result]

          return result
    explanation: |
      **Time Complexity:** O(n * 2^n) — Same as backtracking.

      **Space Complexity:** O(1) auxiliary — We only use the output list (no recursion stack).

      This iterative approach builds subsets incrementally. Starting with `[[]]`, for each new element, we take every existing subset and create a copy with the new element added. After processing all elements, we have all 2^n subsets.

  - approach_name: Bit Manipulation
    is_optimal: true
    code: |
      def subsets(nums: list[int]) -> list[list[int]]:
          n = len(nums)
          result = []

          # Each number from 0 to 2^n - 1 represents a unique subset
          for mask in range(1 << n):  # 1 << n equals 2^n
              subset = []
              for i in range(n):
                  # Check if bit i is set in mask
                  if mask & (1 << i):
                      subset.append(nums[i])
              result.append(subset)

          return result
    explanation: |
      **Time Complexity:** O(n * 2^n) — We iterate through 2^n masks, checking n bits each.

      **Space Complexity:** O(1) auxiliary — No recursion, just loop variables.

      This approach leverages the bijection between subsets and binary numbers. For an array of size n, integers from 0 to 2^n - 1 enumerate all possible subsets. If bit `i` is set in the integer, include `nums[i]` in the subset. This is elegant and avoids recursion entirely.
