TL;DR
Sort, then backtrack and skip same-value siblings at each tree level β O(n Β· 2^n) time, O(n) extra space (beyond the output).
Approach 1 β Brute force: generate everything, dedup with a set
Generate all 2^n subsets exactly as in plain Subsets (or via itertools.combinations of every size), then throw the sorted tuples into a set to kill duplicates.
from itertools import combinations
class Solution:
def subsetsWithDup(self, nums: list[int]) -> list[list[int]]:
nums.sort()
seen: set[tuple[int, ...]] = set()
res: list[list[int]] = []
for k in range(len(nums) + 1):
for combo in combinations(nums, k):
if combo not in seen:
seen.add(combo)
res.append(list(combo))
return res
Complexity: O(n Β· 2^n) time, O(n Β· 2^n) space for the dedup set.
With n <= 10 this actually passes, but it does blind work: for input [2]*10 it materializes 1024 combinations to keep 11, and the hashing/copying overhead grows with every duplicate. The lesson of this problem is to never generate the duplicates at all.
Approach 2 β Backtracking with same-level duplicate skipping
The insight: sort the array so equal values are adjacent. In the backtracking tree, a duplicate subset can only be produced when, at the same tree level (same start), we begin one branch with the first copy of a value and another branch with the second copy β both branches enumerate exactly the same completions. So allow the first occurrence at each level and continue past the rest: if i > start and nums[i] == nums[i-1]: skip. Backtracking is the classical technique of building a partial solution depth-first and undoing (popping) the last choice before trying the next one.
class Solution:
def subsetsWithDup(self, nums: list[int]) -> list[list[int]]:
nums.sort()
res: list[list[int]] = []
path: list[int] = []
def backtrack(start: int) -> None:
res.append(path[:]) # every node of the tree is a subset
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i - 1]:
continue # same value already branched at this level
path.append(nums[i])
backtrack(i + 1)
path.pop()
backtrack(0)
return res
Walkthrough on nums = [1,2,2] (already sorted):
backtrack(0): record [].
i=0, push 1 β backtrack(1): record [1].
- Inside,
i=1, push 2 β backtrack(2): record [1,2]; then i=2, push 2 β record [1,2,2], pop back.
- Back at
start=1, i=2: i > start and nums[2] == nums[1] β skip (this branch would have rebuilt [1,2]). Pop the 1.
- Top level
i=1, push 2 β record [2]; inside i=2 (i == start, allowed), push 2 β record [2,2].
- Top level
i=2: duplicate at the same level β skip.
Result: [[], [1], [1,2], [1,2,2], [2], [2,2]] β six subsets, no dedup pass needed.
Complexity: O(n Β· 2^n) time (at most 2^n nodes, each copies a path of length β€ n), O(n) recursion/path space beyond the output.
Approach 3 β Iterative cascading
The insight: plain Subsets can be built iteratively β for each number, append it to every subset built so far. With duplicates (after sorting), when the current number equals the previous one, append it only to the subsets created in the previous round; extending older subsets would recreate what the first copy already produced.
class Solution:
def subsetsWithDup(self, nums: list[int]) -> list[list[int]]:
nums.sort()
res: list[list[int]] = [[]]
new_start = 0 # index where the previous round's additions begin
for i, x in enumerate(nums):
start = new_start if i > 0 and x == nums[i - 1] else 0
new_start = len(res)
for j in range(start, new_start):
res.append(res[j] + [x])
return res
Walkthrough on [1,2,2]: start res = [[]]. Number 1: extend everything β [[], [1]], new items begin at index 1. Number 2 (not a dup of 1): extend everything β adds [2], [1,2]; new items begin at index 2. Number 2 (dup): extend only indices 2..3 β adds [2,2], [1,2,2]. Final: 6 subsets, same as Approach 2.
Complexity: O(n Β· 2^n) time, O(1) extra space beyond the output.
Common pitfalls
- Forgetting to sort first β the
nums[i] == nums[i-1] skip only works when equal values are adjacent.
- Skipping with
i > 0 instead of i > start β that also suppresses legitimate consecutive picks like [2,2]; the skip must apply only to siblings at the same level, not to a child extending its parent.
- Appending
path instead of path[:] β the same list object gets mutated later, corrupting every recorded subset.
- Deduping at the end with a set of lists β lists arenβt hashable, and converting to tuples is Approach 1βs wasted work in disguise.
Pattern takeaway
When backtracking over a multiset, sort first, then enforce βfirst occurrence onlyβ per tree level: at each choice point, never start two sibling branches with the same value (i > start and nums[i] == nums[i-1] β skip). This one guard converts any subsets/combinations/permutations enumerator into its duplicate-free variant without generating and filtering.