Subsets II (LeetCode 90)
Given an integer array nums that may hold duplicate values, produce the full power set -- every possible subset, including the empty subset and the full array itself.
Because nums can repeat values, two different index choices might describe the same subset of values; the returned list must not contain that subset twice. Return the subsets in any order.
Worked Examples
nums = [1,2,2][[],[1],[1,2],[1,2,2],[2],[2,2]]2 or the second 2 gives the same subset, so [2] and [1,2] appear only once.nums = [0][[],[0]]⚖️Formal Constraints & Bounds
1 <= nums.length <= 10-10 <= nums[i] <= 10
if i > start and nums[i] == nums[i-1]: continueCore Insight
The horizontal duplicate prune if i > start and nums[i] == nums[i-1]: continue skips identical sibling choices at the same recursion depth.
Real-World Scenario & Production Applications
Feature flag combinations testing and database query optimization filter permutations.
Subproblems & Recurrence Decomposition3 Phases
Because every node in the decision tree represents a valid subset, record a deep copy list(path) immediately on entry to every recursive frame.
def dfs(start):
# Every decision tree node is a valid subset
results.append(list(path))Step-by-Step Execution Trace Table
[ ] (Root, ✅ Record []) / | \ choose 1 / choose 2 | choose 2 \ (i=2 > start=0 & nums[2]==nums[1]) / (i=1) | \ [ 1 ] ✅ [ 2 ] ✅ ✂️ PRUNED (Duplicate Sibling) / \ | choose 2 / \ choose 2 (i=2) | choose 2 (i=2) / \ (i>start dup) | [ 1, 2 ] ✅ ✂️ PRUNED [ 2, 2 ] ✅ | choose 2| (i=2) | [ 1, 2, 2 ] ✅ Step-by-Step Decision Trace
- Step 1 (Root, d=0):
path = []Record[]✅. Loopi ∈ [0..2]. - Step 2 (Choose nums[0]=1, d=1):
path = [1]Record[1]✅. Recurse withstart=1. - Step 3 (Choose nums[1]=2, d=2):
path = [1, 2]Record[1, 2]✅. Recurse withstart=2. - Step 4 (Choose nums[2]=2, d=3):
path = [1, 2, 2]Record[1, 2, 2]✅. Loop atstart=3empty. - Step 5 (Backtrack / Pop nums[2], d=2):
path = [1, 2]. Loop at d=2 exhausted. - Step 6 (Backtrack / Pop nums[1], d=1):
path = [1]. Loop at d=1 moves toi=2. - Step 7 (Candidate nums[2]=2, d=2):
i=2 > start=1andnums[2] == nums[1]✂️ PRUNE (Duplicate Sibling). - Step 8 (Backtrack / Pop nums[0], d=0):
path = []. Root loop moves toi=1. - Step 9 (Choose nums[1]=2, d=1):
path = [2]Record[2]✅. Recurse withstart=2. - Step 10 (Choose nums[2]=2, d=2):
path = [2, 2]Record[2, 2]✅. Loop atstart=3empty. - Step 11 (Backtrack / Pop nums[2], d=1):
path = [2]. - Step 12 (Backtrack / Pop nums[1], d=0):
path = []. Root loop moves toi=2. - Step 13 (Candidate nums[2]=2, d=1):
i=2 > start=0andnums[2] == nums[1]✂️ PRUNE (Duplicate Sibling). Search Complete: 6 unique subsets recorded.
nums = [1,2,2]Expected:[[],[1],[1,2],[1,2,2],[2],[2,2]]| 1 | nums.sort(), results = [], path = [] |
| 2 | def backtrack(start): |
| 3 | results.append(list(path)) |
| 4 | for i in range(start, len(nums)): |
| 5 | if i > start and nums[i] == nums[i-1]: |
| 6 | continue |
| 7 | path.append(nums[i]) |
| 8 | backtrack(i + 1) |
| 9 | path.pop() |
| 10 | backtrack(0) |
| 11 | return results |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
[1,1,2] naively produces duplicate subsets like [1] twice — once from each 1. Sorting first, then adding one guard clause, eliminates every duplicate without ever generating and filtering them.
👯 The Analogy: Twins Standing in Line, Only the First Answers the Question
Sort the array so identical values become adjacent, like twins standing next to each other in a line. At any given fork in the decision tree, ask: "should I skip this next value?" If two twins are standing side by side and the first twin already got a turn deciding "should I include this number" at this exact fork, the second identical twin doesn't need to answer the same question again — they'd get an identical answer, producing an identical (duplicate) subset. So only the first twin in each identical run gets asked at that fork; every later twin defers.
🔑 The Key Idea in the Code
The i > start check is crucial: it only skips a duplicate if it's a sibling at the same recursion level, not a duplicate value chosen at a deeper level — [1,1] (both twins included together) is still a valid, distinct subset.
🔑 The Key Takeaway
The i > start condition is the key part of this pattern. It guarantees that you can use duplicate numbers from the input array to build a sequence (parent-to-child depth), but it absolutely prevents you from using identical numbers to start duplicate parallel branches (sibling width).
If the loop at Depth 0 had moved on to i = 1, trying to start a brand new combination with the second 1, then i > start (1 > 0) would be True, and it would successfully skip the duplicate sibling branch.
Skipping duplicates without sorting: Sibling duplicate detection
nums[i] == nums[i-1]requires identical numbers to be adjacent. Forgettingnums.sort()produces duplicate subsets.Using
i > 0instead ofi > start: Guarding duplicate check withif nums[i] == nums[i-1]:withouti > startprunes valid identical elements across different recursion levels (e.g.[2, 2]cannot be formed).Deduplicating post-hoc with
set: Casting subset lists to tuples to store in a hash set wastes memory and time; pruning at the branch level guarantees zero duplicate branches explored.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
The statement asks for "all possible subsets (the power set)", says nums "may contain duplicates" and that the answer "must not contain duplicate subsets", and caps nums.length at 10. Every subset is wanted and the input repeats values: Subsets, with sibling deduplication.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: path always holds exactly the choices from the root to the current node, and is put back to its pre-call state before backtrack returns. Every node (not just a leaf) is a valid subset, so backtrack saves a copy of path on entry, not only at a base case.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
Writing
i > 0instead ofi > startin the skip: it also skips the second1one level deeper, so[1, 1]is lost.Forgetting to sort before the
nums[i] == nums[i - 1]check -- it only catches duplicates that sit next to each other.Deduplicating with a
setof results instead of skipping at the branch -- pays the full 2^N cost and then filters.
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use Subsets. Every node of the decision tree is a valid subset, so backtrack(start) saves a copy of path as soon as it is entered, including the empty one at the root. The loop only picks indices from start onward, so each subset is built in one order and is never produced twice as a reordering. The input can hold duplicates, so I sort it first to put equal values next to each other. Then, inside the loop, I skip nums[i] when i > start and it equals nums[i - 1], because that value was already tried at this level of the tree. The trap is writing i > 0 instead of i > start: that also skips the second 1 when it is picked one level deeper, so for [1, 1, 2] I would lose [1, 1] and [1, 1, 2]. There are up to 2^N subsets, each copied in
O(N), so it's O(N times 2^N) time, withO(N)recursion stack."
Complexity & Mathematical Proof
O(2^N · N)
Look at the execution steps: Sorting takes O(N log N). Backtracking explores 2^N subsets, skipping duplicates via if i > start and nums[i] == nums[i-1]: continue. Copying each subset takes O(N).
O(N) Call Stack
Recursion stack depth equals N; path array has size <= N.
T(N) = O(N log N) + 2^N · O(N) = O(2^N · N)
Derivation Progression
O(N log N)
Groups duplicate elements adjacent in array.
at most 2^N calls
Each call of backtrack saves one subset and loops over the later indices, so there is one call per distinct subset: at most 2^N calls, each copying up to N values.
Variable Definitions
Number of elements in the input array
Memory Architecture & Bounds
O(N) Call stack depth
O(N) Current subset path buffer
O(2^N · N) Array of all subsets
Boundary Best / Worst Cases
when every value is equal: only N + 1 distinct subsets, copied at up to N each
Decision & State-Space Search Tree
Senior SWE Deconstruction & Hardware Caveats
Triggers: "subsets with duplicates", "solution set must not contain duplicate subsets". Sibling deduplication via sorting: when identical numbers are grouped adjacent to each other, you can cleanly skip duplicate choices at the same tree depth.
, values . Power set size .
Memory fails before time: up to subsets of up to values each is output, over 33 million lists at . When the caller only iterates, stream the subsets with a generator instead of building the list, and yield a copy, since path keeps changing after the yield.
Subsets II (LeetCode 90)
Given an integer array nums that may hold duplicate values, produce the full power set -- every possible subset, including the empty subset and the full array itself.
Because nums can repeat values, two different index choices might describe the same subset of values; the returned list must not contain that subset twice. Return the subsets in any order.
Worked Examples
nums = [1,2,2][[],[1],[1,2],[1,2,2],[2],[2,2]]2 or the second 2 gives the same subset, so [2] and [1,2] appear only once.nums = [0][[],[0]]⚖️Formal Constraints & Bounds
1 <= nums.length <= 10-10 <= nums[i] <= 10
if i > start and nums[i] == nums[i-1]: continueCore Insight
The horizontal duplicate prune if i > start and nums[i] == nums[i-1]: continue skips identical sibling choices at the same recursion depth.
Real-World Scenario & Production Applications
Feature flag combinations testing and database query optimization filter permutations.
Subproblems & Recurrence Decomposition3 Phases
Because every node in the decision tree represents a valid subset, record a deep copy list(path) immediately on entry to every recursive frame.
def dfs(start):
# Every decision tree node is a valid subset
results.append(list(path))Step-by-Step Execution Trace Table
[ ] (Root, ✅ Record []) / | \ choose 1 / choose 2 | choose 2 \ (i=2 > start=0 & nums[2]==nums[1]) / (i=1) | \ [ 1 ] ✅ [ 2 ] ✅ ✂️ PRUNED (Duplicate Sibling) / \ | choose 2 / \ choose 2 (i=2) | choose 2 (i=2) / \ (i>start dup) | [ 1, 2 ] ✅ ✂️ PRUNED [ 2, 2 ] ✅ | choose 2| (i=2) | [ 1, 2, 2 ] ✅ Step-by-Step Decision Trace
- Step 1 (Root, d=0):
path = []Record[]✅. Loopi ∈ [0..2]. - Step 2 (Choose nums[0]=1, d=1):
path = [1]Record[1]✅. Recurse withstart=1. - Step 3 (Choose nums[1]=2, d=2):
path = [1, 2]Record[1, 2]✅. Recurse withstart=2. - Step 4 (Choose nums[2]=2, d=3):
path = [1, 2, 2]Record[1, 2, 2]✅. Loop atstart=3empty. - Step 5 (Backtrack / Pop nums[2], d=2):
path = [1, 2]. Loop at d=2 exhausted. - Step 6 (Backtrack / Pop nums[1], d=1):
path = [1]. Loop at d=1 moves toi=2. - Step 7 (Candidate nums[2]=2, d=2):
i=2 > start=1andnums[2] == nums[1]✂️ PRUNE (Duplicate Sibling). - Step 8 (Backtrack / Pop nums[0], d=0):
path = []. Root loop moves toi=1. - Step 9 (Choose nums[1]=2, d=1):
path = [2]Record[2]✅. Recurse withstart=2. - Step 10 (Choose nums[2]=2, d=2):
path = [2, 2]Record[2, 2]✅. Loop atstart=3empty. - Step 11 (Backtrack / Pop nums[2], d=1):
path = [2]. - Step 12 (Backtrack / Pop nums[1], d=0):
path = []. Root loop moves toi=2. - Step 13 (Candidate nums[2]=2, d=1):
i=2 > start=0andnums[2] == nums[1]✂️ PRUNE (Duplicate Sibling). Search Complete: 6 unique subsets recorded.
nums = [1,2,2]Expected:[[],[1],[1,2],[1,2,2],[2],[2,2]]| 1 | nums.sort(), results = [], path = [] |
| 2 | def backtrack(start): |
| 3 | results.append(list(path)) |
| 4 | for i in range(start, len(nums)): |
| 5 | if i > start and nums[i] == nums[i-1]: |
| 6 | continue |
| 7 | path.append(nums[i]) |
| 8 | backtrack(i + 1) |
| 9 | path.pop() |
| 10 | backtrack(0) |
| 11 | return results |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
[1,1,2] naively produces duplicate subsets like [1] twice — once from each 1. Sorting first, then adding one guard clause, eliminates every duplicate without ever generating and filtering them.
👯 The Analogy: Twins Standing in Line, Only the First Answers the Question
Sort the array so identical values become adjacent, like twins standing next to each other in a line. At any given fork in the decision tree, ask: "should I skip this next value?" If two twins are standing side by side and the first twin already got a turn deciding "should I include this number" at this exact fork, the second identical twin doesn't need to answer the same question again — they'd get an identical answer, producing an identical (duplicate) subset. So only the first twin in each identical run gets asked at that fork; every later twin defers.
🔑 The Key Idea in the Code
The i > start check is crucial: it only skips a duplicate if it's a sibling at the same recursion level, not a duplicate value chosen at a deeper level — [1,1] (both twins included together) is still a valid, distinct subset.
🔑 The Key Takeaway
The i > start condition is the key part of this pattern. It guarantees that you can use duplicate numbers from the input array to build a sequence (parent-to-child depth), but it absolutely prevents you from using identical numbers to start duplicate parallel branches (sibling width).
If the loop at Depth 0 had moved on to i = 1, trying to start a brand new combination with the second 1, then i > start (1 > 0) would be True, and it would successfully skip the duplicate sibling branch.
Skipping duplicates without sorting: Sibling duplicate detection
nums[i] == nums[i-1]requires identical numbers to be adjacent. Forgettingnums.sort()produces duplicate subsets.Using
i > 0instead ofi > start: Guarding duplicate check withif nums[i] == nums[i-1]:withouti > startprunes valid identical elements across different recursion levels (e.g.[2, 2]cannot be formed).Deduplicating post-hoc with
set: Casting subset lists to tuples to store in a hash set wastes memory and time; pruning at the branch level guarantees zero duplicate branches explored.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
The statement asks for "all possible subsets (the power set)", says nums "may contain duplicates" and that the answer "must not contain duplicate subsets", and caps nums.length at 10. Every subset is wanted and the input repeats values: Subsets, with sibling deduplication.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: path always holds exactly the choices from the root to the current node, and is put back to its pre-call state before backtrack returns. Every node (not just a leaf) is a valid subset, so backtrack saves a copy of path on entry, not only at a base case.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
Writing
i > 0instead ofi > startin the skip: it also skips the second1one level deeper, so[1, 1]is lost.Forgetting to sort before the
nums[i] == nums[i - 1]check -- it only catches duplicates that sit next to each other.Deduplicating with a
setof results instead of skipping at the branch -- pays the full 2^N cost and then filters.
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use Subsets. Every node of the decision tree is a valid subset, so backtrack(start) saves a copy of path as soon as it is entered, including the empty one at the root. The loop only picks indices from start onward, so each subset is built in one order and is never produced twice as a reordering. The input can hold duplicates, so I sort it first to put equal values next to each other. Then, inside the loop, I skip nums[i] when i > start and it equals nums[i - 1], because that value was already tried at this level of the tree. The trap is writing i > 0 instead of i > start: that also skips the second 1 when it is picked one level deeper, so for [1, 1, 2] I would lose [1, 1] and [1, 1, 2]. There are up to 2^N subsets, each copied in
O(N), so it's O(N times 2^N) time, withO(N)recursion stack."
Complexity & Mathematical Proof
O(2^N · N)
Look at the execution steps: Sorting takes O(N log N). Backtracking explores 2^N subsets, skipping duplicates via if i > start and nums[i] == nums[i-1]: continue. Copying each subset takes O(N).
O(N) Call Stack
Recursion stack depth equals N; path array has size <= N.
T(N) = O(N log N) + 2^N · O(N) = O(2^N · N)
Derivation Progression
O(N log N)
Groups duplicate elements adjacent in array.
at most 2^N calls
Each call of backtrack saves one subset and loops over the later indices, so there is one call per distinct subset: at most 2^N calls, each copying up to N values.
Variable Definitions
Number of elements in the input array
Memory Architecture & Bounds
O(N) Call stack depth
O(N) Current subset path buffer
O(2^N · N) Array of all subsets
Boundary Best / Worst Cases
when every value is equal: only N + 1 distinct subsets, copied at up to N each
Decision & State-Space Search Tree
Senior SWE Deconstruction & Hardware Caveats
Triggers: "subsets with duplicates", "solution set must not contain duplicate subsets". Sibling deduplication via sorting: when identical numbers are grouped adjacent to each other, you can cleanly skip duplicate choices at the same tree depth.
, values . Power set size .
Memory fails before time: up to subsets of up to values each is output, over 33 million lists at . When the caller only iterates, stream the subsets with a generator instead of building the list, and yield a copy, since path keeps changing after the yield.