Backtracking and Bit Manipulation Approaches for Generating Subsets
You can generate all subsets of an array using either a recursive backtracking approach that builds subsets by including or excluding each element, or an iterative bit manipulation approach that maps each subset to a binary mask.
The power set (all possible subsets) of a collection is a fundamental problem in algorithmic interviews. In the kdn251/interviews repository, the backtracking approach is implemented in Subsets.java and SubsetsII.java, while bit manipulation techniques appear in related bitwise problems. This guide examines both approaches with concrete code examples from the repository.
Backtracking Approach for Generating Subsets
The backtracking approach uses depth-first search (DFS) to explore the binary decision tree of including or excluding each element. This method is explicitly implemented in the repository's LeetCode solutions to handle both distinct and duplicate elements.
Classic Implementation in Subsets.java
The file leetcode/array/Subsets.java demonstrates the standard pattern. The subsets() method initializes the result list and invokes the recursive recurse() helper, which uses a Stack<Integer> named path to track the current subset.
// leetcode/array/Subsets.java
public class Subsets {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
recurse(result, nums, new Stack<>(), 0);
return result;
}
// classic backtrack: push → recurse → pop
private void recurse(List<List<Integer>> result, int[] nums,
Stack<Integer> path, int position) {
if (position == nums.length) {
result.add(new ArrayList<>(path));
return;
}
// choose current element
path.push(nums[position]);
recurse(result, nums, path, position + 1);
// backtrack: undo the choice
path.pop();
// skip current element
recurse(result, nums, path, position + 1);
}
}
Handling Duplicates in SubsetsII.java
When the input contains duplicate numbers, the basic backtracking approach generates duplicate subsets. The leetcode/array/SubsetsII.java file solves this by first sorting the array, then skipping repeated elements during recursion using the guard if (i > idx && nums[i] == nums[i-1]) continue.
// leetcode/array/SubsetsII.java
public class SubsetsII {
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums); // sort to group duplicates
List<List<Integer>> result = new ArrayList<>();
helper(nums, new ArrayList<>(), 0, result);
return result;
}
private void helper(int[] nums, List<Integer> cur,
int idx, List<List<Integer>> res) {
res.add(new ArrayList<>(cur));
for (int i = idx; i < nums.length; i++) {
if (i > idx && nums[i] == nums[i - 1]) continue; // skip dup
cur.add(nums[i]);
helper(nums, cur, i + 1, res);
cur.remove(cur.size() - 1); // backtrack
}
}
}
Bit Manipulation Approach for Generating Subsets
The bit manipulation approach treats each subset as a binary number where each bit represents the presence or absence of an element. While the repository does not contain a dedicated subsets implementation using this method, the technique is demonstrated in related bitwise problems such as MaximumProductOfWordLengths.
Binary Mask Technique
For an array of length N, there are 2^N possible subsets. Each integer from 0 to (1 << N) - 1 represents a unique binary mask. The i-th bit of the mask indicates whether element i is included in the current subset.
Implementation Example
// Bit-mask iterative generation of all subsets
public static List<List<Integer>> subsetsBitMask(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
int n = nums.length;
int total = 1 << n; // 2ⁿ possible masks
for (int mask = 0; mask < total; mask++) {
List<Integer> subset = new ArrayList<>();
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) { // i-th bit is set → include nums[i]
subset.add(nums[i]);
}
}
result.add(subset);
}
return result;
}
Complexity Comparison
Both approaches generate 2^N subsets, but they differ in auxiliary space and implementation overhead.
- Backtracking: Runs in O(N · 2^N) time to visit all subsets and copy them into the result. It requires O(N) auxiliary space for the recursion stack and the current
pathstack, plus O(N · 2^N) to store the output. - Bit Manipulation: Also runs in O(N · 2^N) time, as each of the
2^Nmasks requires scanningNbits. However, it uses only O(1) extra space (excluding the output list), making it more memory-efficient for deep recursion scenarios.
Summary
- Backtracking uses DFS recursion to build subsets by making binary include/exclude decisions at each element, implemented in
Subsets.javaandSubsetsII.java. - Bit manipulation treats subsets as binary masks from
0to2^N-1, iterating through all possible combinations without recursion. - The repository prefers backtracking for its flexibility in handling duplicates and early pruning, while bit manipulation offers a compact iterative alternative.
- Both approaches run in O(N · 2^N) time, but backtracking requires O(N) auxiliary stack space versus O(1) for bit manipulation.
Frequently Asked Questions
What is the time complexity of generating subsets using backtracking?
The time complexity is O(N · 2^N), where N is the length of the input array. There are 2^N total subsets, and copying each subset of average length N/2 into the result list requires linear time per subset.
How does bit manipulation generate subsets without recursion?
Bit manipulation maps each subset to a unique integer mask between 0 and 2^N-1. The i-th bit of the mask indicates whether element i is included. By iterating through all integers in this range and checking each bit with (mask & (1 << i)) != 0, the algorithm constructs each subset iteratively without function call overhead.
Can backtracking handle duplicate elements in the input array?
Yes, backtracking can handle duplicates by sorting the array first and then skipping repeated elements during the recursive exploration. As shown in SubsetsII.java, the condition if (i > idx && nums[i] == nums[i-1]) continue; prevents the algorithm from adding the same value at the same recursion depth, ensuring unique subsets only.
When should I use bit manipulation over backtracking for subset generation?
Choose bit manipulation when you need a compact, iterative solution with O(1) auxiliary space, particularly for small input sizes (N ≤ 30) where the mask fits in a standard integer. Prefer backtracking when you need to handle duplicates, apply early pruning, or extend the logic to other combinatorial problems like permutations or combinations, as demonstrated in the kdn251/interviews repository.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →