Power Set
Problem Statement
Given an array of distinct integers, generate all possible subsets (the power set). The power set includes the empty set and the set itself.
Example:
Input: [1, 2, 3]
Output: [[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]]
Explanation: For 3 elements, there are 2^3 = 8 subsets.
Input: [1, 2]
Output: [[], [1], [2], [1,2]]
Noob-Friendly Explanation
Imagine you're packing for a trip and you have 3 items: a hat, sunglasses, and a scarf. You want to list every possible combination of items you could bring:
- Bring nothing: []
- Bring just hat: [hat]
- Bring just sunglasses: [sunglasses]
- Bring just scarf: [scarf]
- Bring hat + sunglasses: [hat, sunglasses]
- Bring hat + scarf: [hat, scarf]
- Bring sunglasses + scarf: [sunglasses, scarf]
- Bring all three: [hat, sunglasses, scarf]
That's 8 combinations for 3 items (2³ = 8). This is the power set!
The bit manipulation trick: for 3 items, use numbers 0 to 7 (000 to 111 in binary). Each bit represents whether an item is included (1) or not (0). Number 5 = 101 means include item 0 and item 2 but not item 1.
Approach 1: Brute Force (Recursion / Backtracking)
Intuition
For each element, we have two choices: include it or exclude it. Recursively make this choice for each element.
Algorithm
- Start with an empty subset
- For each element, branch into two paths: include it or skip it
- When we've decided for all elements, add the current subset to the result
import java.util.*;
class Solution {
public static List<List<Integer>> powerSet(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, 0, new ArrayList<>(), result);
return result;
}
private static void backtrack(int[] nums, int index, List<Integer> current,
List<List<Integer>> result) {
// Base case: considered all elements
if (index == nums.length) {
result.add(new ArrayList<>(current));
return;
}
// Choice 1: Don't include nums[index]
backtrack(nums, index + 1, current, result);
// Choice 2: Include nums[index]
current.add(nums[index]);
backtrack(nums, index + 1, current, result);
current.remove(current.size() - 1); // backtrack
}
public static void main(String[] args) {
int[] nums = {1, 2, 3};
List<List<Integer>> result = powerSet(nums);
for (List<Integer> subset : result) {
System.out.println(subset);
}
}
}Complexity Analysis
- Time Complexity: O(N * 2^N) — There are 2^N subsets, each takes O(N) to copy
- Space Complexity: O(N * 2^N) — Storing all subsets
Approach 2: Optimal (Bit Manipulation)
Intuition
For an array of N elements, there are 2^N subsets. Each number from 0 to 2^N - 1 represents a unique subset when viewed in binary. The i-th bit tells us whether to include the i-th element.
Example for [1, 2, 3]:
0 = 000 → []
1 = 001 → [1]
2 = 010 → [2]
3 = 011 → [1, 2]
4 = 100 → [3]
5 = 101 → [1, 3]
6 = 110 → [2, 3]
7 = 111 → [1, 2, 3]
Algorithm
- Compute total = 2^N (use
1 << n) - For each number i from 0 to total - 1:
- Check each bit of i
- If bit j is set, include nums[j] in the subset
- Add the subset to result
import java.util.*;
class Solution {
public static List<List<Integer>> powerSet(int[] nums) {
int n = nums.length;
int total = 1 << n; // 2^n
List<List<Integer>> result = new ArrayList<>();
for (int mask = 0; mask < total; mask++) {
List<Integer> subset = new ArrayList<>();
for (int j = 0; j < n; j++) {
// Check if j-th bit is set in mask
if ((mask & (1 << j)) != 0) {
subset.add(nums[j]);
}
}
result.add(subset);
}
return result;
}
public static void main(String[] args) {
int[] nums = {1, 2, 3};
List<List<Integer>> result = powerSet(nums);
for (List<Integer> subset : result) {
System.out.println(subset);
}
// []
// [1]
// [2]
// [1, 2]
// [3]
// [1, 3]
// [2, 3]
// [1, 2, 3]
}
}Complexity Analysis
- Time Complexity: O(N * 2^N) — 2^N subsets, each taking O(N) to generate
- Space Complexity: O(N * 2^N) — Storing all subsets
Key Takeaways
- 2^N Subsets: An array of N elements always has exactly 2^N subsets
- Bit Masking: Each number from 0 to 2^N-1 represents a unique subset
- Check Bit j:
(mask & (1 << j)) != 0tells if element j is in the subset - Both Approaches Same Complexity: Recursion and bit manipulation have the same time/space complexity, but bit manipulation is iterative and often cleaner
- Limitation: Bit manipulation works for N ≤ 30 (since 2^30 ≈ 10^9). For larger N, neither approach is feasible