Subsets – 78. LeetCode

The Subsets problem is classic combinatorics tasks in which you’re given a list of unique integers, and your goal is to return all possible subsets of the list (including the empty subset of the list itself). It’s a fundamental problem that helps build a deeper understanding of recursion, backtracking, and iterative approaches in solving combinatorial problems.

Brute Force

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        if(nums == null || nums.length == 0){
            return result;
        }
        List<List<Integer>> result = new ArrayList<>();
        result.add(new ArrayList<>()); // Start with an empty subset
        for (int num : nums) {
            int size = result.size();
            for (int i = 0; i < size; i++) {
                List<Integer> newSubset = new ArrayList<>(result.get(i));
                newSubset.add(num); // Add the current number to the existing subset
                result.add(newSubset); // Add the new subset to the result
            }
        }
        return result;
    }
}

• At each step, the algorithm takes all subsets that have been generated so far (result) and extends them by adding the current number (num).

• New subsets are created by “cascading” or “layering” the current number onto existing subsets.

• For example:

• Start with [].

• Add 1 to produce [[], [1]].

• Add 2 to produce [[], [1], [2], [1, 2]].

• Add 3 to produce [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]].

Optimized Approach


The subsets problem aligns perfectly with backtracking because it systematically explores all possible combinations by including or excluding each element, making it an intuitive and efficient way to solve the problem. Here’s why:

Systematic Exploration: Backtracking divides the problem into smaller subproblems, allowing the algorithm to explore each possible subset by including or excluding the current element.

Efficient State Management: The algorithm dynamically builds subsets without the need for excessive memory or pre-computation, leveraging recursion to manage state.

Flexible and Adaptable: Backtracking naturally handles variations, such as generating subsets with constraints (e.g., subsets of a certain size).

Avoids Redundancy: Unlike brute force approaches that may generate duplicate subsets through repeated iterations, backtracking ensures no duplicate computation.


This makes backtracking not only a better algorithm for generating subsets but also a general-purpose approach for solving a wide variety of combinatorial problems efficiently and elegantly.

public class Subsets {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(nums, 0, new ArrayList<>(), result);
        return result;
    }

    private void backtrack(int[] nums, int index, List<Integer> current, List<List<Integer>> result) {
        // Add the current subset to the result
        result.add(new ArrayList<>(current));

        // Iterate through remaining elements
        for (int i = index; i < nums.length; i++) {
            current.add(nums[i]); // Include nums[i]
            backtrack(nums, i + 1, current, result); // Recurse with next index
            current.remove(current.size() - 1); // Backtrack (remove nums[i])
        }
    }
}

Line-By-Line Explanation

Step 1: Method Signature

public List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    backtrack(nums, 0, new ArrayList<>(), result);
    return result;
}
  • Defines the main method subsets that returns all possible subsets of the input array nums
  • Initializes an empty list result to store the final subsets

Step 2: Backtracking Function Signature

private void backtrack(int[] nums, int index, List<Integer> current, List<List<Integer>> result) {
  • Defines a helper method backtrack that generates subsets recursively.
  • Facilitates recursive exploration of subsets, maintaining a clear state (current) and tracking progress via index.

Step 3: Add Current Subset

result.add(new ArrayList<>(current));
  • Adds a copy of the current subet (current) to the result list.
  • Every recursive call represents a valid subset of nums. This ensures all subsets are added to the result.

Step 4: Iterate Over Remaining Elements

for (int i = index; i < nums.length; i++) {
  • Iterates through all elements in nums, starting from the current index.
  • Ensures that subsets are genrated systematically:
    • Each element in nums is considered for inclusion
    • Starting from index prevents duplicate subsets
  • Starts iteration at index to ensure no duplicate subsets are generated, avoiding redundant computations.

Step 5: Include Current Element

current.add(nums[i]); // Include nums[i]
  • Adds the current element to the subset under construction.

Step 6: Recurse with Next Index

backtrack(nums, i + 1, current, result); // Recurse with next index
  • Recursively generates subsets by advancing to the next index (i + 1).
  • This explores all subsets that include the current element.

Step 7: Backtrack

current.remove(current.size() - 1); // Backtrack (remove nums[i])
  • Removes the last element added to current, effectively “undoing” the decision to include nums[i].
  • Restores current to its previous state before exploring the next possibility in the loop.
  • Ensures that subsets generated in the current recursion level don’t interfere with subsets in subsequent recursion levels.

Why This Code Works


This code systematically generates subsets using backtracking, adhering to LeetCode best practices by being modular, efficient, and clean. Its use of recursion ensures flexibility, while explicit state management avoids unnecessary overhead. This approach is an ideal example of how to solve combinatorial problems effectively.

Leave a Reply

Your email address will not be published. Required fields are marked *