Subsets

Medium
9 days ago

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.

For example:

nums = [1,2,3] Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

nums = [0] Output: [[],[0]]

Constraints:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • All the numbers of nums are unique.
Sample Answer
## Subsets (Power Set) - LeetCode Problem

This problem asks us to generate all possible subsets (the power set) of a given array of unique integers. The solution set must not contain duplicate subsets, and the order of the subsets doesn't matter.

### 1. Brute Force Approach

The simplest approach is to iterate through all possible combinations of elements in the input array. We can do this by generating all binary numbers from 0 to 2^n - 1, where n is the number of elements in the input array. Each bit in the binary number represents whether the corresponding element in the array is present in the current subset.

```python
def subsets_brute_force(nums):
    n = len(nums)
    result = []
    for i in range(2**n):
        subset = []
        for j in range(n):
            if (i >> j) & 1:
                subset.append(nums[j])
        result.append(subset)
    return result

# Example usage:
nums = [1, 2, 3]
print(subsets_brute_force(nums))

Explanation:

  • We iterate through all numbers from 0 to 2^n - 1.
  • For each number, we check each bit. If the j-th bit is set (1), we include the j-th element from the input array in the current subset.

2. Optimal Approach: Backtracking

A more efficient approach is to use backtracking. We can build subsets recursively. At each step, we have a choice: either include the current element in the subset or exclude it. This leads to a tree-like exploration of all possible subsets.

def subsets_backtracking(nums):
    result = []
    n = len(nums)

    def backtrack(index, current_subset):
        result.append(current_subset[:])  # Add a copy to avoid modification

        for i in range(index, n):
            current_subset.append(nums[i])
            backtrack(i + 1, current_subset)
            current_subset.pop()  # Backtrack: remove the last element

    backtrack(0, [])
    return result

# Example usage:
nums = [1, 2, 3]
print(subsets_backtracking(nums))

Explanation:

  • The backtrack function recursively explores all possible subsets.
  • It starts at a given index in the input array.
  • It adds the current subset to the result.
  • Then, for each element from index to the end of the array, it includes the element in the current_subset, recursively calls backtrack to explore further subsets, and then removes the element (backtracks) to explore subsets without that element.

3. Big(O) Runtime Analysis (Backtracking)

The time complexity of the backtracking solution is O(N * 2^N), where N is the number of elements in the input array nums. Here's why:

  • 2^N Subsets: There are 2^N possible subsets of a set with N elements. The algorithm must generate each of these subsets.
  • O(N) to Copy Subsets: In the backtrack function, result.append(current_subset[:]) takes O(N) time because it creates a copy of the current_subset. We need to copy it because the list is passed by reference and we don't want to keep the reference to the current subset around.

Therefore, the overall time complexity is O(N * 2^N).

4. Big(O) Space Usage Analysis (Backtracking)

The space complexity of the backtracking solution is O(N), excluding the space used to store the output subsets.

  • Call Stack: The maximum depth of the recursion is N (the number of elements in the array). This is because, in the worst case, we might include all N elements in a subset before backtracking. The call stack will store N function calls.
  • current_subset: The current_subset list will, at most, store N elements.

Output Space: The space to store all the subsets is O(N * 2^N), but this is typically not considered for the space complexity analysis (we don't count output space complexity).

Therefore, the auxiliary space complexity (excluding the output) is O(N).

5. Edge Cases and Handling

  • Empty Input: If the input array is empty, the function should return a list containing only the empty set: [[]]. Both brute force and backtracking solutions correctly handle this case.
  • Duplicate Numbers: The problem states that the input array contains unique elements. If the input array contained duplicate numbers, the result set would contain duplicate subsets. To handle duplicate numbers, we would need to sort the input array and add a condition to the backtracking algorithm to skip duplicate elements.
  • Negative Numbers: The input array can contain negative numbers, and the algorithms should work correctly without any modifications, as we are simply dealing with combinations of numbers.

Here's an example of how we might handle duplicates (although the problem states there won't be any):

def subsets_with_duplicates(nums):
    result = []
    nums.sort()
    n = len(nums)

    def backtrack(index, current_subset):
        result.append(current_subset[:])

        for i in range(index, n):
            if i > index and nums[i] == nums[i-1]:
                continue # Skip duplicate elements
            current_subset.append(nums[i])
            backtrack(i + 1, current_subset)
            current_subset.pop()

    backtrack(0, [])
    return result