Given an integer array that may contain duplicates, return all possible subsets without duplicates in the result. Sort first so duplicate values are adjacent, then on the exclude branch skip repeated values at the same recursion depth with the guard i > start && nums[i] === nums[i-1].
Given nums = [1, 2, 2], return every unique subset (the power set, minus duplicates). Order inside subsets and between subsets does not matter. Expected output: [[], [1], [1,2], [1,2,2], [2], [2,2]] — six subsets, not eight. Without deduplication the two 2s would each produce an identical copy of [2] and [1,2].
i > start && nums[i] === nums[i-1], skip nums[i]. This prunes the branch that would start a duplicate subset — identical first choices at the same depth produce identical subtrees, so we only keep the first.nums.sort() puts equal elements next to each other, making the duplicate-skip guard possible.result.push([...current]) at the top of backtrack — every prefix of every path is a valid subset (including the empty set on the first call).start. For i = start to end: this avoids re-using elements before the current position and prevents subsets from appearing in multiple orders.i > start (not the first choice at this depth) and nums[i] === nums[i-1], continue. The i > start part is crucial — it allows the first occurrence of a value to proceed normally.backtrack(i + 1), pop. Classic backtracking template.i > start, not i > 0. Using i > 0 would wrongly skip the second 2 even when it's the first element chosen at a deeper level, eliminating valid subsets like [1,2,2].In the worst case (all unique elements) we still touch all 2ⁿ subsets. With many duplicates the tree is smaller: k distinct values means at most 2ᵏ unique subsets. Sorting costs O(n log n), which is dominated by the exponential enumeration. Each subset is copied in O(n) to result, giving the · n factor. Space is O(n) for the recursion stack (depth = n).
[1, 2, 2]. We explore every include/exclude choice, recording each prefix as a subset.1▶function subsetsWithDup(nums: number[]): number[][] {2▶ nums.sort((a, b) => a - b); // sort so duplicates are adjacent3▶ const result: number[][] = [];4▶ const current: number[] = [];56 function backtrack(start: number): void {7 result.push([...current]); // always record — every prefix is valid89 for (let i = start; i < nums.length; i++) {10 // skip duplicate: same value as previous at this depth level11 if (i > start && nums[i] === nums[i - 1]) continue;1213 current.push(nums[i]); // include nums[i]14 backtrack(i + 1); // recurse with next index15 current.pop(); // exclude nums[i] (backtrack)16 }17 }1819 backtrack(0);20 return result;21}
function subsetsWithDup(nums: number[]): number[][] {
nums.sort((a, b) => a - b); // sort so duplicates are adjacent
const result: number[][] = [];
const current: number[] = [];
function backtrack(start: number): void {
result.push([...current]); // always record — every prefix is valid
for (let i = start; i < nums.length; i++) {
// skip duplicate: same value as previous at this depth level
if (i > start && nums[i] === nums[i - 1]) continue;
current.push(nums[i]); // include nums[i]
backtrack(i + 1); // recurse with next index
current.pop(); // exclude nums[i] (backtrack)
}
}
backtrack(0);
return result;
}nums[i] === nums[i-1] check would miss some duplicates.result.push([...current]) runs before the loop, so the empty set is captured on the very first call, and every intermediate prefix is captured on subsequent calls. The spread [...current] creates a snapshot; without it all entries would alias the same mutable array.i > startmeans we're past the first choice at this recursion level. Combined with nums[i] === nums[i-1], this says: “I already explored the subtree rooted at the previous copy of this value — skip this one.” The guard fires on the exclude path (when we loop back after popping), not on the include path.backtrack(i + 1) (move forward so each element is used at most once per subset), then pop to undo and try the next candidate. Using i + 1 instead of start + 1 ensures we advance past the current position each time.k distinct values, the tree has at most 2ᵏ leaves, so heavy duplicates shrink the real runtime significantly.nums first — iterating left-to-right naturally produces lexicographic order.[[]] and for each number extend existing subsets — but skip if the number equals the previous and the previous was just added in this round (track the range of newly added subsets).target parameter, stop recursing when the running sum exceeds target, and only record when it equals target exactly — the same sort-and-skip guard handles duplicates identically.backtrack(i + 1) to backtrack(i) to allow re-picking the same element.function subsetsWithDup(nums: number[]): number[][] {
nums.sort((a, b) => a - b); // sort so duplicates are adjacent
const result: number[][] = [];
const current: number[] = [];
function backtrack(start: number): void {
result.push([...current]); // always record — every prefix is valid
for (let i = start; i < nums.length; i++) {
// skip duplicate: same value as previous at this depth level
if (i > start && nums[i] === nums[i - 1]) continue;
current.push(nums[i]); // include nums[i]
backtrack(i + 1); // recurse with next index
current.pop(); // exclude nums[i] (backtrack)
}
}
backtrack(0);
return result;
}k distinct values, the tree has at most 2ᵏ leaves, so heavy duplicates shrink the real runtime significantly.Enumerate every one of the 2ⁿ subsets via bitmasks, sort each one so equal subsets share a canonical form, and drop repeats with a Set of string keys.
function subsetsWithDup(nums: number[]): number[][] {
nums.sort((a, b) => a - b); // canonical element order
const seen = new Set<string>();
const result: number[][] = [];
// Each bitmask 0..2^n-1 picks a subset of indices.
for (let mask = 0; mask < (1 << nums.length); mask++) {
const subset: number[] = [];
for (let i = 0; i < nums.length; i++) {
if (mask & (1 << i)) subset.push(nums[i]);
}
const key = subset.join(','); // sorted nums => stable dedupe key
if (!seen.has(key)) {
seen.add(key);
result.push(subset);
}
}
return result;
}2ⁿ subsets even when duplicates make many of them identical, then pays for hashing every subset to filter them. Backtracking with the i > start && nums[i] === nums[i-1] skip never generates a duplicate in the first place.| "may contain duplicates" + "unique subsets" | sort + i > start skip guard |
| power set (all subsets) | backtrack recording every prefix |
| "no duplicate subsets" in combination/permutation problem | sort + skip-duplicate pattern |
| Combination Sum II / Permutations II | same sort-and-skip template |
nums.sort((a, b) => a - b);
const result: number[][] = [];
const current: number[] = [];
function backtrack(start: number): void {
result.push([...current]);
for (let i = start; i < nums.length; i++) {
if (i > start && nums[i] === nums[i - 1]) continue;
current.push(nums[i]);
backtrack(i + 1);
current.pop();
}
}
backtrack(0);
return result;nums = [1, 2, 2], how many unique subsets does subsetsWithDup return?i > start && nums[i] === nums[i-1]. What happens if you write i > 0 instead of i > start?nums = [2, 2]. Which subsets does the algorithm produce?