Subsets II
A medium problem from Backtracking. Try it first, open a hint when you are stuck, and read the solution when you are done.
- Medium
- Backtracking
The problem
You are given a list of integers, nums, in which a value may appear more than once. Return every different subset of it. A subset is what is left when you keep some of the values and drop the others; keeping none gives the empty subset. Two subsets are the same when they hold the same values the same number of times, so in [1, 2, 2] keeping the first 2 or the second 2 gives one subset, [2], which is listed once. The subsets may come in any order, and so may the values inside a subset.
What to write
function solve(nums: number[]): number[][]Example 1
Input nums = [1,2,2] Output [[],[1],[2],[1,2],[2,2],[1,2,2]]
Three positions can be kept or dropped in 8 ways, but two results turn up twice: either 2 alone gives [2], and 1 with either 2 gives [1, 2]. That leaves 6 different subsets: [], [1], [2], [1, 2], [2, 2] and [1, 2, 2].
Example 2
Input nums = [0] Output [[],[0]]
A single value can be dropped or kept. Dropping it gives the empty subset [] and keeping it gives [0]. With one value nothing can repeat, so both are listed. The value 0 counts like any other value.
Constraints
Input may contain duplicates; 1 ≤ nums.length ≤ 10.
Hints
Each hint gives away a little more. Open one only when you need it.
Hint 1: Use the constraints
There are at most 10 values, so at most 1,024 subsets, few enough to write out. But values can repeat, and then different choices of positions give the same subset: in [1, 2, 2], keeping the first 2 or the second 2 both give [2]. Each different subset must appear once.
Hint 2: Ask a smaller question
List the subsets of [2, 2] by choosing positions: neither, the first, the second, or both. Two of the four results are the same list. Which one choice could you forbid so that the repeat is never written, while [2, 2] still is?
Hint 3: Name the pattern
The signal is "return every different subset". The plan for a list without repeats still works: record the current subset, add a value, explore, take the value out. That is backtracking. Repeated values are the new part. Sort first, so that equal values end up next to each other and a repeat is easy to spot. Read the lesson on backtracking.
Hint 4: State the invariant
Among the values a call can add next, it uses only the first of each group of equal values. Adding the second 2 in the same place would rebuild subsets that the first 2 already gave. A deeper call may still add that second 2, which is how [2, 2] appears.
Hint 5: Sketch the pseudocode
Sort
nums. Keep an answer list and one working subset. extend(start): record a copy of the subset. For each position i from start on: if i is past start and the value at i equals the value at i - 1, skip it. Otherwise add the value, call extend(i + 1) and remove the value. Begin with extend(0).
Solution
From here on the page gives the method away. If you are still solving, go back to the hints.
Approach
Sort first and skip equal sibling choices at each recursion depth. Equal values may still be chosen at deeper levels.
The slow way
Build every subset as if all the values were different, then sort each one and remove those you have already seen. It is correct, but it builds lists only to delete them, and comparing whole lists is extra work. [1, 2, 2] gives 8 lists to keep 6, and ten equal values give 1,024 lists to keep 11.
The insight
A repeat appears only when two equal values compete for the same place in a subset. After sorting, equal values are neighbors, so that moment is easy to see: the loop is about to try a value equal to the one it tried a moment ago. Skip it there. Deeper down the same value is still welcome, so a subset can hold it several times.
Step by step
- Sort the values so that equal ones sit next to each other. The skip further down compares a value only with its left neighbor. That catches every repeat only if equal values sit side by side, as in [0, 4, 4] and not in [4, 0, 4].
- Keep a list for the answers and one list for the current subset. Every call works on the same current subset, adding a value and taking it out again. The answers list gets a copy each time, because the shared subset keeps changing.
- Each call first records a copy of the current subset. Every subset counts, not only the longest ones, so a call records what it was handed before it adds anything. The start position tells the call which values it may still add.
- Try each value from the start position on. Only values after the last one added are offered, so a subset is always built from left to right and never in two different orders.
- Skip a value equal to the one before it in the same loop. The loop already tried an equal value in this place, along with everything that can follow it. Trying its twin here would build the same subsets a second time.
- Add the value, extend from the next position, take the value out. The deeper call starts right after this value, so it may add an equal value that follows: that is how [2, 2] is built. Taking the value out lets the loop try another value in this place.
- Start at the first position with an empty subset. The first call records the empty subset and may add any value. When it returns, every different subset has been recorded once.
The same numbered steps are the comments in each solution.
JavaScript
function solve(nums) {
// 1. Sort the values so that equal ones sit next to each other
nums.sort((a, b) => a - b);
// 2. Keep a list for the answers and one list for the current subset
const out = [],
path = [];
// 3. Each call first records a copy of the current subset
function visit(start) {
out.push([...path]);
// 4. Try each value from the start position on
for (let i = start; i < nums.length; i++) {
// 5. Skip a value equal to the one before it in the same loop
if (i > start && nums[i] === nums[i - 1]) continue;
// 6. Add the value, extend from the next position, take the value out
path.push(nums[i]);
visit(i + 1);
path.pop();
}
}
// 7. Start at the first position with an empty subset
visit(0);
return out;
}TypeScript
function solve(nums: number[]): number[][] {
// 1. Sort the values so that equal ones sit next to each other
nums.sort((a, b) => a - b);
// 2. Keep a list for the answers and one list for the current subset
const out: number[][] = [],
path: number[] = [];
// 3. Each call first records a copy of the current subset
function visit(start: number): void {
out.push([...path]);
// 4. Try each value from the start position on
for (let i = start; i < nums.length; i++) {
// 5. Skip a value equal to the one before it in the same loop
if (i > start && nums[i] === nums[i - 1]) continue;
// 6. Add the value, extend from the next position, take the value out
path.push(nums[i]);
visit(i + 1);
path.pop();
}
}
// 7. Start at the first position with an empty subset
visit(0);
return out;
}
Python
def solve(nums):
# 1. Sort the values so that equal ones sit next to each other
nums.sort()
# 2. Keep a list for the answers and one list for the current subset
result = []
path = []
# 3. Each call first records a copy of the current subset
def backtrack(start):
result.append(path.copy())
# 4. Try each value from the start position on
for i in range(start, len(nums)):
# 5. Skip a value equal to the one before it in the same loop
if i > start and nums[i] == nums[i - 1]:
continue
# 6. Add the value, extend from the next position, take the value out
path.append(nums[i])
backtrack(i + 1)
path.pop()
# 7. Start at the first position with an empty subset
backtrack(0)
return result
Example 1, traced
Input nums = [1,2,2] Output [[],[1],[2],[1,2],[2,2],[1,2,2]]
| Subset so far | Values from the start position | What happens |
|---|---|---|
| [] | 1, 2, 2 | Records []. Then adds 1 |
| [1] | 2, 2 | Records [1]. Then adds the first 2 |
| [1, 2] | 2 | Records [1, 2]. Then adds the second 2 |
| [1, 2, 2] | none | Records [1, 2, 2]. Nothing is left to add |
| [1] | 2, 2 | Skips the second 2: it would build [1, 2] again |
| [2] | 2 | Records [2]. Then adds the second 2 |
| [2, 2] | none | Records [2, 2]. Nothing is left to add |
| [] | 1, 2, 2 | Skips the second 2: it would build [2] again |
Why it works
After sorting, one rule picks a single way to build each subset: take values from left to right, and from a group of equal values always take the leftmost ones. The loop only moves right, and the skip refuses a value whose equal left neighbor was on offer in the same place. So each different subset is built exactly once, and it is recorded when its call starts.
Time and space
Time O(n · 2^n), extra space O(n + output). One call runs per different subset, and n values have at most 2^n subsets. A call copies up to n values and loops over up to n positions: O(n · 2^n) time. The calls nest at most n + 1 deep and share one working subset: O(n + output) space.
n and m are input sizes, h is a tree's height, V and E are a graph's vertices and edges. The bounds describe this reference solution.
Watch out
- Sort before you skip. The skip compares only neighbors, so in [4, 0, 4] the two 4s never meet: [4] is recorded twice, and [4, 0] and [0, 4] both appear.
- Skip an equal value only in the loop that offered its twin. If every value equal to its left neighbor is skipped, two equal values never share a subset: [1, 2, 2] loses [2, 2] and [1, 2, 2].
- Record the subset at the start of every call, not only when no value is left. If you record only at the end, [1, 2, 2] returns [1, 2, 2] and [2, 2] and loses the other four subsets.
Pattern
Backtracking. Builds on 3Sum and Subsets.
Keep going
9 more problems solved with backtracking
- Generate ParenthesesMedium
- SubsetsMedium
- Combination SumMedium
- PermutationsMedium
- Combination Sum IIMedium
- Word SearchMedium
- Palindrome PartitioningMedium
- Letter Combinations of a Phone NumberMedium
- N QueensHard