Subsets
BFS expansion or DFS backtracking. Every subset and permutation problem is a variation. append → recurse → pop is the universal backtrack pattern.
Building subsets naively = combinatorial explosion. BFS one-liner doubles the list on each element: result += [s+[num] for s in result]
The BFS insight: each new element either is or isn't in a subset. For every existing subset, we create one copy with the element appended. Starting from [[]], adding element 1 gives [[], [1]]. Adding 2 gives [[], [1], [2], [1,2]]. Adding 3 gives all 8 subsets. For permutations and constrained generation (parentheses, BSTs), DFS backtracking is cleaner.
BFS Expansion vs DFS Backtracking
Know which approach fits the problem before writing any code.
Use when: Subsets, combinations, power set. Order doesn't matter. No constraints on path.
How: Start with [[]]. For each element, extend every existing subset. result += [s+[x] for s in result]
Use when: Permutations, constrained generation (parens, abbreviations). Order matters or constraints mid-path.
How: append → recurse → pop. Base case returns or records result. Prune with constraint checks.
The 3-Step Framework
One pattern covers both BFS and DFS variants.
For subsets: start result=[[]], loop over each num. In one line, double the result list by appending num to every existing subset. For duplicates: sort first, only extend subsets added in the LAST round (tracked via a start index).
result = [[]]
for num in nums:
result += [s + [num] for s in result]
# Duplicates:
for i, num in enumerate(nums):
if i > 0 and nums[i] == nums[i-1]:
new = [s + [num] for s in result[start:]]
else:
start = len(result)
new = [s + [num] for s in result]
result += newFor DFS backtracking: add current element to path, recurse with updated state, then remove (backtrack). The recursion naturally explores all branches. Record result at the right depth (every node for subsets, leaf only for permutations).
def backtrack(start, path):
result.append(list(path)) # subsets: record every node
for i in range(start, n):
path.append(nums[i])
backtrack(i + 1, path) # next must come after i
path.pop() # backtrack
# Permutations: record only at leaf
def bt(path, remaining):
if not remaining:
result.append(list(path))
return
for i, x in enumerate(remaining):
path.append(x)
bt(path, remaining[:i]+remaining[i+1:])
path.pop()Balanced parentheses: open < n (can open), close < open (can close). This prunes the tree from exponential to Catalan(n) valid outputs. Abbreviations, case variations: branch based on character type. Evaluate expression: split on operators and recurse left/right.
# Balanced parentheses pruning
def bt(s, open, close):
if len(s) == 2*n: result.append(s); return
if open < n:
bt(s+'(', open+1, close)
if close < open: # can only close unclosed
bt(s+')', open, close+1)
# Case permutations pruning
if ch.isdigit():
bt(idx+1, path+[ch]) # one branch
else:
bt(idx+1, path+[ch.lower()])
bt(idx+1, path+[ch.upper()])Pattern Recognition
'All possible', 'generate', 'enumerate' → Subsets pattern.