Subsets
title: Subsets tags: subset looping-through-input-and-output nested-double-for-loop backtracking include-exclude-object
Given an integer array nums, return all possible subsets (the power set). The solution set must not contain duplicate subsets.
Example 1:
Input: nums = [1,2,3]
Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
Example 2:
Input: nums = [0]
Output: [[],[0]]
Constraints
- 1 <= nums.length <= 10
- -10 <= nums[i] <= 10
All the numbers of nums are unique.
Thinking Process
- Classify the problem: This is 0-1 object selection problem.
- No duplicate subsets. Any order.
Define the interface.
Input: [1,2,3]
Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
Brute Force Approach
Go through two loop, start with the first element and go through the next element. 1, 2 and 3 Add 1, 1-2 and 1-3 and so on
Time and Space Complexity
T: O(N2) S: O(1)
For n = 0
[]
For n = 1
[1] [include the output for n = 0 and [1]] [[], [1]]
For n = 2
[1,2] [include the output for n = 1 and [2], [1,2]] [[], [1], [2], [1,2]]
n = 3
[1,2,3] [include the output for n = 0, n = 1 as well as n = 2] [[],[1],[2],[1,2], [3],[1,3],[2,3],[1,2,3]]
Generating the Subset
How do we generate the subset?
- For n = 1, build an outer list and add the subset to this list.
What should we add to the outer list?
- Inner list contains an empty list and just one element with given n.
- For n = 2
- Add 2 to empty list.
- Add 2 to [1].
Start with an empty set, iterate through all elements one-by-one, and add them to existing sets to create new subsets.
Solution Outline
- Initialize the output array to an array with empty array.
- If n = 1, add an empty array and an array with that n value to the output and return that as result. output = [[], ]
- From n = 2 to n, traverse through the output array. Add the 2 to all the arrays inside the output array.
- Return the output.
Iterative Solution
|
|
You cannot modify the output array within the loop, so subset stores the generated element and we generate a new element and append it to the output array.
Time Complexity
In each step, the number of subsets doubles as each element is added to all the existing subsets. Therefore, there is a total of O(2 Nāā) subsets, where N is the total number of elements in the input set. Since we construct a new subset from an existing set, therefore, the time complexity is O(N * 2N).
Space Complexity
The additional space is used for the output list. Since we will have a total of O(2 N) subsets and each subset can take up to O(N) space, therefore, the space complexity is O(N * 2^N).
Recursive Approach
We need a helper method: Why? Because, we need to pass the next element that we need to add to the subset.
We need to have two recursive calls in the helper. One for:
- Include
- Exclude
Unit of Work
- What is the slice of work that each recursive call must do?
- Do I need to include the element or exclude the element?
- The parent will actually control what index the child should process.
Base Case
When we reach the leaf node, I have generated one subset. It can be added to the output.
Time and Space Complexity
Time: O(N 2N) Space: O(N 2N)
|
|
Refactored Solution
|
|
Alternative Solution
|
|
Output: ["", “x”, “y”, “xy”]
A subset of a set S is a set that contains some or all elements of S. The subsets of a set can be found using recursion or bitwise operations.
For example, the subsets of {1, 2, 3} are {}, {1}, {2}, {3}, {1,2}, {1,3}, {2,3}, {1,2,3}.
Java - Recursive approach:
|
|
C++ - Bit manipulation:
|
|
Python - Iterative approach:
|
|
Finding all subsets of a set is useful for combinatorics and backtracking algorithms.
Finding all subsets of a given set is a classic problem that can be solved using a recursive approach or by using bit manipulation. Here’s a simple guide to generating all possible subsets of a set using both methods:
Method 1: Recursion
We can use a recursive function to explore all the possible subsets. We’ll choose or not choose each element and recursively call the function for the rest of the elements.
Here’s a Python code snippet to achieve this:
|
|
Method 2: Bit Manipulation
Since there are (2^n) subsets of a set with (n) elements, we can use the binary representation of numbers from 0 to (2^n - 1) to represent all the subsets.
Here’s a Python code snippet for this method:
|
|
In this code snippet, (i & (1 << j)) != 0
checks if the j-th bit of i is set, which corresponds to including the j-th element of the set in the current subset.
Both of these methods have a time complexity of (O(2^n \times n)), as there are (2^n) subsets, and it takes (O(n)) time to generate each subset. The space complexity is also (O(2^n \times n)), as that’s the total size of the output.
Define the interface Input: [1,2,3] Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
Constraints: 1 <= nums.length <= 10 -10 <= nums[i] <= 10 All the numbers of nums are unique.
No duplicate subsets. Any order.
Classify the Problem 0-1 Bounded Knapsack Problem
Input: [1,2,3] Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]] Brute Force Go through two loop, start with the first element and go through the next element. 1, 2 and 3 Add 1, 1-2 and 1-3 and so on
T: O(N^2) S: O(1)
Recursive Approach Helper method: Why? We need to pass the next element that we need to add to the subset.
We need to have two recursive calls in the helper
- Include
- Exclude
Unit of Work What is the slice of work that each recursive call must do? Do I need to include the element or exclude the element. The parent will actually control what index the child should process.
Base Case When we reach the leaf node, I have generated one subset It can be added to the output
|
|
n = 1 [1] [[], [1]]
n = 2 [1,2] [include the output for n = 1 and [2], [1,2]] [[], [1], [2], [1,2]]
n = 3 [1,2,3] [include the output for n = 1 as well as n = 2, ] [[],[1],[2],[1,2], [3],[1,3],[2,3],[1,2,3]]
How do we generate the subset?
- For n = 1, build an outer list and add the subset to this list
What should we add to the outer list?
- Inner list contains an empty list and just one element with given n.
- For n = 2
Add 2 to empty list
Add 2 to [1]
- Intialize the output array to an empty array.
- If n = 1, add an empty array and an array with that n value to the output and return that as result. output = [[], ]
- From n = 2 to n, Traverse through the output array Add the 2 to all the arrays inside the output array
- Return the output
[1,2,3]
|
|
n = 1 [1] [[], [1]]
n = 2 [1,2] [include the output for n = 1 and [2], [1,2]] [[], [1], [2], [1,2]]
n = 3 [1,2,3] [include the output for n = 1 as well as n = 2, ] [[],[1],[2],[1,2], [3],[1,3],[2,3],[1,2,3]]
How do we generate the subset?
- For n = 1, build an outer list and add the subset to this list
What should we add to the outer list?
- Inner list contains an empty list and just one element with given n.
- For n = 2
Add 2 to empty list
Add 2 to [1]
- Intialize the output array to an empty array.
- If n = 1, add an empty array and an array with that n value to the output and return that as result. output = [[], ]
- From n = 2 to n, Traverse through the output array Add the 2 to all the arrays inside the output array
- Return the output
[1,2,3]
=end
def subsets(nums) output = [[]]
for i in (0..nums.size-1)
for j in (0..output.size-1)
subset = output[j]
output << subset + [nums[i]]
end
end
return output
end
|
|
Identifying Problem Isomorphism
“Subsets” has an approximate isomorphism to “Combination Sum”.
In “Subsets”, the task is to find all possible subsets of a given set of unique integers, while in “Combination Sum” you are to find all unique combinations of a set of candidate numbers where the sum equals a target number.
The reasoning for this mapping is that both problems involve enumerating all possible combinations (subsets or sums) from a set of numbers. “Combination Sum” is a constrained version of “Subsets”, where the constraint is that the sum of the numbers in each combination must equal the target number. Thus, although not an exact isomorphism, the problems share a significant amount of structure, especially in the way they can both be solved using recursive backtracking. The strategy for “Subsets” could inform your approach to “Combination Sum”, and vice versa.
Problem Classification
This problem can be classified based on the problem domain as follows:
Combinatorics: The problem involves finding all possible subsets of a set, which is a basic problem in the field of combinatorics.
Set Theory: The concepts of subsets and power sets come from set theory. The constraint that the solution set must not contain duplicate subsets further emphasizes this aspect.
Array Manipulation: The input for this problem is given as an array of integers, indicating that array manipulation will be a part of the problem-solving process.
Constraint Satisfaction: The problem involves satisfying the specific constraint that the solution set must not contain duplicate subsets.
This classification gives an overview of the types of strategies and concepts that might be useful when approaching this problem.
Language Agnostic Coding Drills
Here are the smallest units of learning that could be separated from this piece of code, arranged in an increasing level of difficulty:
Defining Functions: Understand how to define a function and how to use its arguments. Recognize the importance of indentation in defining the function body.
Basic Data Types (Lists): Understand what a list is and how to create one. Learn about list methods like
append()
,copy()
, andpop()
.Conditional Statements (if): Understand the use of
if
statements for conditionally executing code.Recursion: Understand what recursion is and how it’s used to solve problems in programming. Recognize when a problem can be solved using recursion.
Class Definition: Learn what a class is and how to define one in Python. Understand what a method is and how it’s defined within a class. Get to know what
self
refers to in a method.Understanding of Python’s List Comprehensions: Learn the syntax and usage of list comprehensions for creating new lists.
Type Hinting: Understand how to use type hints to make your Python code more self-explanatory.
Problem Solving with Recursion and Backtracking: Recognize when a problem can be solved using a combination of recursion and backtracking. This involves choosing a candidate (adding an element to the current set in this case), exploring the associated subproblem (recursive function call), and then undoing the choice (removing the element from the current set).
Solution for Coding Drills in Python
Here are the Python code solutions for each of the drills listed above:
- Defining Functions:
|
|
- Basic Data Types (Lists):
|
|
- Conditional Statements (if):
|
|
- Recursion:
|
|
- Class Definition:
|
|
- Understanding of Python’s List Comprehensions:
|
|
- Type Hinting:
|
|
- Problem Solving with Recursion and Backtracking:
|
|