LeetCode (8): Backtracking Algorithm
Chen Kai BOSS

Backtracking is one of the most elegant and powerful algorithmic paradigms for solving constraint satisfaction problems. Unlike brute force that blindly explores all possibilities, backtracking builds solutions incrementally and abandons partial solutions ("backtracks") as soon as they cannot lead to a valid answer. This makes it perfect for problems like generating permutations, finding combinations, solving puzzles (N-Queens, Sudoku), and exploring decision trees. This guide teaches you the backtracking framework through five classic LeetCode problems, shows you how to identify when to use backtracking, demonstrates pruning techniques to optimize your solutions, and provides a reusable template that works across 90% of backtracking problems. We'll also cover complexity analysis, common pitfalls, and answer 10 frequently asked questions.

Series Navigation

📚 LeetCode Algorithm Masterclass Series (10 Parts): 1. Hash Tables (Two Sum, Longest Consecutive, Group Anagrams) 2. Two Pointers Techniques (Collision pointers, fast-slow, sliding window) 3. Linked List Operations (Reverse, cycle detection, merge) 4. Binary Tree Traversal & Recursion (Inorder/Preorder/Postorder, LCA) 5. Dynamic Programming Intro (1D/2D DP, state transition) 6. Binary Search Advanced (Integer/Real binary search, answer binary search) 7. → Backtracking Algorithms (Permutations, combinations, pruning) ← You are here 8. Stack & Queue (Monotonic stack, priority queue, deque) 9. Graph Algorithms (BFS/DFS, topological sort, union-find) 10. Greedy & Bit Manipulation (Greedy strategies, bitwise tricks)


What is Backtracking?

Backtracking is a systematic method for solving problems by trying partial solutions and then abandoning them ("backtracking") if they cannot be completed to a valid solution. It's essentially a refined form of brute force search that uses constraints to prune the search space.

Key characteristics:

  • Incremental construction: Builds solutions step by step
  • Constraint checking: Validates partial solutions before continuing
  • Pruning: Abandons paths that cannot lead to valid solutions
  • Recursive exploration: Uses recursion to explore the solution space

When to use backtracking:

  • Problems asking for "all possible" solutions (permutations, combinations, subsets)
  • Constraint satisfaction problems (N-Queens, Sudoku)
  • Problems with exponential search space that can be pruned
  • Decision problems where you need to explore multiple paths

Backtracking vs DFS: Understanding the Relationship

Many beginners confuse backtracking with DFS (Depth-First Search). Here's the key distinction:

  • Purpose: Traverse or search a graph/tree structure
  • Focus: Visit all nodes, find paths, explore structure
  • State: Usually doesn't modify the graph structure
  • Example: Finding if a path exists from node A to node B

Backtracking

  • Purpose: Solve constraint satisfaction problems
  • Focus: Build valid solutions incrementally
  • State: Modifies and restores state (this is crucial!)
  • Example: Finding all valid N-Queen placements

The relationship: Backtracking uses DFS as its exploration mechanism, but adds: 1. State modification: Make a choice (add element to solution) 2. Constraint checking: Verify if current path is valid 3. State restoration: Undo the choice (backtrack) if invalid or after exploring

Visual analogy:

  • DFS: Walking through a maze, marking visited rooms
  • Backtracking: Placing chess pieces on a board, removing them if the placement leads to an invalid configuration

The Three Essential Elements of Backtracking

Every backtracking problem has three core components:

1. Choices (What decisions can we make?)

At each step, identify what choices are available. For example:

  • Permutations: Which number to place at position ?
  • N-Queens: Which column to place the queen in row?
  • Subsets: Include the current element or not?

2. Constraints (What makes a choice valid?)

Define what makes a partial solution valid:

  • Permutations: No duplicates in the current path
  • N-Queens: No two queens attack each other
  • Combination Sum: Sum doesn't exceed target, no duplicates

3. Goal (When have we found a solution?)

Determine when a complete solution is found:

  • Permutations: All positions filled
  • N-Queens: Allqueens placed
  • Subsets: All elements processed (or sum equals target)

Template thinking:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def backtrack(path, choices):
# Goal: Is this a complete solution?
if is_goal(path):
result.append(path.copy()) # Save solution
return

# Explore choices
for choice in choices:
# Constraint: Is this choice valid?
if is_valid(choice, path):
# Make choice
path.append(choice)
# Recurse
backtrack(path, updated_choices)
# Backtrack: undo choice
path.pop()


Universal Backtracking Template

Here's a reusable template that works for most backtracking problems:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def backtrack_template():
result = []

def backtrack(path, choices):
# Base case: goal reached
if is_goal(path):
result.append(path[:]) # Copy, don't reference!
return

# Explore all choices
for choice in choices:
# Pruning: skip invalid choices early
if not is_valid(choice, path):
continue

# Make choice
path.append(choice)
# Update choices for next level (if needed)
new_choices = update_choices(choices, choice)

# Recurse
backtrack(path, new_choices)

# Backtrack: undo choice
path.pop()

backtrack([], initial_choices)
return result

Critical points: 1. Copy paths: Use path[:] or path.copy() when saving solutions 2. Restore state: Always undo changes after recursion (path.pop()) 3. Prune early: Check constraints before recursing 4. Update choices: Some problems require modifying available choices


LeetCode 46: Permutations

Problem Statement

Given an array nums of distinct integers, return all possible permutations. You can return the answer in any order.

Example:

  • Input: nums = [1,2,3]
  • Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

Constraints:

- - - All integers are distinct

Problem Analysis

Permutations problem requires generating all possible orderings of array elements. Fordistinct elements, there arepermutations.

Core Challenges:

  1. Avoiding duplicate selections: Each element can appear only once in each permutation
  2. Systematically exploring all possibilities: Need to try all possible permutation combinations
  3. Efficient backtracking: When a path cannot continue, need to undo choices and try alternatives

Use Cases:

  • Password cracking: Try all possible password combinations
  • Task scheduling: Arrange task execution order
  • Game solving: Explore all possible game states

Solution Approach

Three Elements Analysis:

  • Choices: Which number to place at current position? (All unused numbers)
  • Constraints: Cannot select numbers already used (tracked by used array)
  • Goal: Path length equals array length (complete permutation generated)

Algorithm Flow:

  1. Use used array to mark which numbers have been used
  2. Use path list to record current path (partial permutation)
  3. Recursive function backtrack():
    • If path length equals nums length, save result and return
    • Iterate through all numbers, if unused:
      • Mark as used
      • Add to path
      • Recursively process next position
      • Backtrack: undo mark and choice

Complexity Analysis

Time Complexity: - Totalpermutations (leaf nodes) - Each permutation takestime to copy to result - Total time: Space Complexity: - Recursion stack depth:(at mostlevels) - path array: - used array: - Result array:(output space, not counted)

Complexity Proof:

For permutation ofelements: - Level 1 haschoices - Level 2 haschoices - ... - Levelhaschoice

Total permutations:Each permutation needstime to copy, so total time complexity is.

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from typing import List

class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
"""
Generate all permutations of array

Algorithm Steps:
1. Initialize result list, path list, and used array
2. Define backtrack function:
a. Termination: path length equals array length, save result
b. Iterate through all numbers, skip used ones
c. Make choice: mark used, add to path
d. Recurse: process next position
e. Undo choice: restore mark, remove from path

Boundary Conditions:
- Empty array: return [[]]
- Single element: return [[element]]

Optimization Techniques:
- Use used array to avoid duplicate selection (O(1) lookup)
- Use path[:] to create copy when saving result (avoid reference issue)

Variable Meanings:
- result: List storing all permutations
- path: Current permutation being built
- used: Boolean array, used[i] indicates if nums[i] is used
"""
result = []
path = []
used = [False] * len(nums) # Mark which numbers are used

def backtrack():
# Termination: path length equals array length
if len(path) == len(nums):
result.append(path[:]) # Create copy, avoid reference issue
return

# Iterate through all possible choices
for i in range(len(nums)):
# Constraint check: skip used numbers
if used[i]:
continue

# Make choice
path.append(nums[i])
used[i] = True

# Recurse: process next position
backtrack()

# Undo choice (backtrack)
path.pop()
used[i] = False

backtrack()
return result

Alternative using set:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def permute_set(nums):
result = []

def backtrack(path):
if len(path) == len(nums):
result.append(path[:])
return

for num in nums:
if num in path: # Constraint check
continue
path.append(num)
backtrack(path)
path.pop()

backtrack([])
return result

Complexity:

  • Time:
  • There arepermutations, and each takesto copy
  • Space:
  • Recursion depth and path storage

LeetCode 39: Combination Sum

Problem Statement

Given an array of distinct integers candidates and a target integer target, return a list of all unique combinations of candidates where the chosen numbers sum to target. You may return the combinations in any order. The same number may be chosen from candidates an unlimited number of times.

Example:

  • Input: candidates = [2,3,6,7], target = 7
  • Output: [[2,2,3],[7]]

Constraints:

- - - All elements are distinct -

Problem Analysis

Combination Sum requires finding all combinations that sum to target, with each number reusable unlimited times.

Core Challenges:

  1. Avoiding duplicate combinations: [2,3] and [3,2] are the same combination
  2. Controlling search space: Early termination when remaining value becomes negative
  3. Allowing reuse: Same number can appear multiple times in combination

Key Insight:

By limiting choice range (starting from start), we avoid duplicates. Always starting from array beginning would produce both [2,3] and [3,2]. By only choosing from current position onward, we ensure numbers in combination follow array order, avoiding duplicates.

Solution Approach

Three Elements Analysis:

  • Choices: Which number to add to current combination? (Starting from start position)
  • Constraints:
    • Combination sum cannot exceed target (remain >= 0)
    • Only choose from current position onward (avoid duplicates)
  • Goal: Combination sum equals target (remain == 0)

Algorithm Flow:

  1. Define backtrack function backtrack(start, remain):
    • start: Starting index for current choices
    • remain: Remaining target sum
  2. Termination conditions:
    • remain == 0: Found valid combination, save result
    • remain < 0: Current path invalid, return immediately
  3. Iterate through all candidates starting from start:
    • Add to current path
    • Recurse: backtrack(i, remain - candidates[i]) (start from i, allow reuse)
    • Backtrack: Remove current choice

Complexity Analysis

Time Complexity:, where - Worst case, each position has two choices (select or not) - Due to reuse allowed, actual search space may be larger - Pruning can significantly reduce actual nodes searched

Space Complexity: - Recursion stack depth at most target (when always selecting smallest number) - path array length at most target

Complexity Proof:

Assuming minimum candidate is, need to select at mostnumbers. Each position haschoices, but due to pruning and reuse constraints, actual complexity is betweenand, where.

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from typing import List

class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
"""
Find all combinations summing to target (numbers reusable)

Algorithm Steps:
1. Define backtrack function backtrack(start, remain):
a. Termination 1: remain == 0, save result
b. Termination 2: remain < 0, prune and return
c. Iterate candidates starting from start
d. Make choice: add to path
e. Recurse: backtrack(i, remain - candidates[i]) (from i, allow reuse)
f. Undo choice: remove from path

Boundary Conditions:
- Empty array: return []
- target is 0: return [[]]
- All candidates > target: return []

Optimization Techniques:
- Use start parameter to avoid duplicate combinations
- Prune early when remain < 0, reduce invalid search
- Sorting enables further optimization (see optimized version below)

Variable Meanings:
- result: List storing all valid combinations
- path: Current combination being built
- start: Starting index for current choices (avoid duplicates)
- remain: Remaining target sum
"""
result = []
path = []

def backtrack(start, remain):
# Termination 1: Found valid combination
if remain == 0:
result.append(path[:]) # Save result
return

# Termination 2: Current path invalid (pruning)
if remain < 0:
return

# Choose from start onward, avoid duplicate combinations
for i in range(start, len(candidates)):
# Make choice
path.append(candidates[i])

# Recurse: Start from i (not i+1), allow reuse
backtrack(i, remain - candidates[i])

# Undo choice (backtrack)
path.pop()

backtrack(0, target)
return result

Algorithm Principle:

Why starting from start avoids duplicates:

Assume candidates = [2,3,6,7], target = 7:

  • If starting from 0: May produce [2,3] and [3,2] (duplicate)
  • If starting from start:
    • After selecting 2, start=0, can only select 2,3,6,7 → produces [2,2,3], [2,3] etc.
    • After selecting 3, start=1, can only select 3,6,7 → produces [3,3] etc.
    • Won't produce [3,2] because when selecting 3, start=1, won't select 2 again

Why recurse from i (not i+1):

Because problem allows reusing same number. If starting from i+1, each number can be used only once, which becomes "Combination Sum II" problem.

Common Pitfalls:

Pitfall 1: Starting from 0 causes duplicates

1
2
3
4
5
6
7
8
# ❌ Wrong
for i in range(len(candidates)): # Always starting from 0
backtrack(i, remain - candidates[i])
# Produces duplicates like [2,3] and [3,2]

# ✅ Correct
for i in range(start, len(candidates)): # Start from start
backtrack(i, remain - candidates[i])

Pitfall 2: Using i+1 prevents reuse

1
2
3
4
5
# ❌ Wrong
backtrack(i + 1, remain - candidates[i]) # Each number usable once

# ✅ Correct
backtrack(i, remain - candidates[i]) # Allow reuse

LeetCode 78: Subsets

Problem Statement

Given an integer array nums of unique elements, return all possible subsets (the power set). The solution set must not contain duplicate subsets. Return the solution in any order.

Example:

  • Input: nums = [1,2,3]
  • Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

Constraints:

- - - All elements are unique

Analysis

Three elements:

  • Choices: Include current element or skip it?
  • Constraints: No constraints (all subsets are valid)
  • Goal: Processed all elements

Key insight: At each element, we have two choices: include it or exclude it. This creates a binary tree of decisions.

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def subsets(nums):
result = []
n = len(nums)

def backtrack(path, start):
# Goal: save current subset (all paths are valid)
result.append(path[:])

# Try including each remaining element
for i in range(start, n):
path.append(nums[i])
backtrack(path, i + 1)
path.pop()

backtrack([], 0)
return result

Alternative: binary decision approach:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def subsets_binary(nums):
result = []
n = len(nums)

def backtrack(path, index):
# Goal: processed all elements
if index == n:
result.append(path[:])
return

# Choice 1: skip current element
backtrack(path, index + 1)

# Choice 2: include current element
path.append(nums[index])
backtrack(path, index + 1)
path.pop()

backtrack([], 0)
return result

Complexity:

  • Time: -subsets, each copied in
  • Space:
  • Recursion depth

LeetCode 51: N-Queens

Problem Statement

The n-queens puzzle is the problem of placingqueens on anchessboard such that no two queens attack each other. Given an integer, return all distinct solutions to the n-queens puzzle. You may return the answer in any order.

Each solution contains a distinct board configuration of the n-queens' placement, where 'Q' and '.' indicate a queen and an empty space, respectively.

Example:

  • Input: n = 4
  • Output: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]

Constraints:

-

Analysis

Three elements:

  • Choices: Which column to place queen in row?
  • Constraints:
    • No two queens in same column
    • No two queens in same diagonal (both main and anti-diagonal)
  • Goal: Allqueens placed

Key insight:

  • Place queens row by row (one per row)
  • Track used columns and diagonals
  • Diagonals: row - col (main) and row + col (anti-diagonal) are constant

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def solveNQueens(n):
result = []
board = [['.'] * n for _ in range(n)]

# Track constraints
used_cols = set()
used_diag1 = set() # row - col
used_diag2 = set() # row + col

def backtrack(row):
# Goal: all queens placed
if row == n:
# Convert board to required format
solution = [''.join(row) for row in board]
result.append(solution)
return

# Try each column
for col in range(n):
diag1 = row - col
diag2 = row + col

# Constraint check
if col in used_cols or diag1 in used_diag1 or diag2 in used_diag2:
continue

# Make choice
board[row][col] = 'Q'
used_cols.add(col)
used_diag1.add(diag1)
used_diag2.add(diag2)

# Recurse
backtrack(row + 1)

# Backtrack
board[row][col] = '.'
used_cols.remove(col)
used_diag1.remove(diag1)
used_diag2.remove(diag2)

backtrack(0)
return result

Optimization: Using boolean arrays instead of sets (faster for small):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def solveNQueens_optimized(n):
result = []
board = [['.'] * n for _ in range(n)]
used_cols = [False] * n
used_diag1 = [False] * (2 * n - 1) # row - col ranges from -(n-1) to (n-1)
used_diag2 = [False] * (2 * n - 1) # row + col ranges from 0 to 2(n-1)

def backtrack(row):
if row == n:
result.append([''.join(row) for row in board])
return

for col in range(n):
diag1 = row - col + n - 1 # Shift to non-negative index
diag2 = row + col

if used_cols[col] or used_diag1[diag1] or used_diag2[diag2]:
continue

board[row][col] = 'Q'
used_cols[col] = True
used_diag1[diag1] = True
used_diag2[diag2] = True

backtrack(row + 1)

board[row][col] = '.'
used_cols[col] = False
used_diag1[diag1] = False
used_diag2[diag2] = False

backtrack(0)
return result

Complexity:

  • Time:
  • First row haschoices, second has at most, etc.
  • Space:
  • Board storage +recursion depth

LeetCode 22: Generate Parentheses

Problem Statement

Givenpairs of parentheses, write a function to generate all combinations of well-formed parentheses.

Example:

  • Input: n = 3
  • Output: ["((()))","(()())","(())()","()(())","()()()"]

Constraints:

-

Analysis

Three elements:

  • Choices: Add '(' or ')'?
  • Constraints:
    • Can only add '(' if count of '(' < - Can only add ')' if count of ')' < count of '('
  • Goal: Length equalsand string is valid

Key insight: Track counts of open and close parentheses. Only add ')' when there are unmatched '('.

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def generateParenthesis(n):
result = []

def backtrack(path, open_count, close_count):
# Goal: length is 2n
if len(path) == 2 * n:
result.append(''.join(path))
return

# Choice 1: add '(' if possible
if open_count < n:
path.append('(')
backtrack(path, open_count + 1, close_count)
path.pop()

# Choice 2: add ')' if valid (more opens than closes)
if close_count < open_count:
path.append(')')
backtrack(path, open_count, close_count + 1)
path.pop()

backtrack([], 0, 0)
return result

Alternative using string concatenation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def generateParenthesis_string(n):
result = []

def backtrack(s, open_count, close_count):
if len(s) == 2 * n:
result.append(s)
return

if open_count < n:
backtrack(s + '(', open_count + 1, close_count)

if close_count < open_count:
backtrack(s + ')', open_count, close_count + 1)

backtrack('', 0, 0)
return result

Why this works: By ensuring close_count < open_count before adding ')', we guarantee that every ')' has a matching '(' to its left, making all generated strings valid.

Complexity:

  • Time:
  • Catalan number
  • Space:
  • Recursion depth

Pruning Optimization Techniques

Pruning is crucial for making backtracking efficient. Here are common techniques:

1. Early Termination

Stop exploring when constraints are violated:

1
2
3
# Example: Combination Sum
if current_sum > target:
return # Prune: can never reach target

2. Constraint Propagation

Use constraints to reduce choices:

1
2
3
4
5
# Example: N-Queens
# Only try columns not in used_cols
for col in range(n):
if col in used_cols:
continue # Skip invalid columns

3. Memoization (for overlapping subproblems)

Cache results of subproblems:

1
2
3
4
5
6
7
8
# Example: Word Break II (advanced)
memo = {}
def backtrack(s, wordDict):
if s in memo:
return memo[s]
# ... backtrack logic
memo[s] = result
return result

4. Sort and Skip Duplicates

Sort input and skip duplicate choices:

1
2
3
4
5
6
# Example: Combination Sum II (with duplicates)
candidates.sort()
for i in range(start, n):
# Skip duplicates
if i > start and candidates[i] == candidates[i-1]:
continue

5. Bounds Checking

Check if remaining choices can satisfy constraints:

1
2
3
4
# Example: Subset Sum
remaining_sum = sum(nums[start:])
if current_sum + remaining_sum < target:
return # Prune: even using all remaining elements won't reach target

Complexity Analysis

Understanding backtracking complexity is crucial for interviews:

Time Complexity

General formula:where:

-= branching factor (choices per node) -= depth of recursion -= cost per solution (usually copying)

Examples:

  • Permutations: -leaves,to copy each
  • Subsets: -subsets,to copy each
  • N-Queens:
  • Pruned significantly from
  • Generate Parentheses:
  • Catalan number

Space Complexity

Components: 1. Recursion stack:whereis depth 2. Path storage:for current path 3. Auxiliary structures:for tracking (used sets, arrays) 4. Result storage:whereis number of solutions,is size per solution

Total: Usuallyexcluding result storage


Common Pitfalls and Debugging Tips

Pitfall 1: Not Copying Paths

Wrong:

1
result.append(path)  # Adds reference, not copy!

Correct:

1
result.append(path[:])  # or path.copy()

Pitfall 2: Forgetting to Backtrack

Wrong:

1
2
3
path.append(choice)
backtrack(path, choices)
# Missing: path.pop()

Correct:

1
2
3
path.append(choice)
backtrack(path, choices)
path.pop() # Always restore state!

Pitfall 3: Modifying Choices Incorrectly

Wrong (for permutations):

1
2
3
choices.remove(choice)  # Modifies original list
backtrack(path, choices)
choices.append(choice) # Hard to restore

Correct:

1
2
3
used[i] = True
backtrack(path, used)
used[i] = False # Easy to restore

Pitfall 4: Wrong Constraint Checking Order

Always check constraints before making the choice:

Wrong:

1
2
3
path.append(choice)
if not is_valid(path): # Too late!
return

Correct:

1
2
3
if not is_valid(choice, path):
continue # Skip before adding
path.append(choice)

Debugging Checklist

  1. ✅ Are you copying paths when saving solutions?
  2. ✅ Are you restoring state after recursion?
  3. ✅ Are constraints checked before making choices?
  4. ✅ Are you pruning invalid paths early?
  5. ✅ Is the base case correct?
  6. ✅ Are choices being updated correctly for next level?

Q&A: 10 Frequently Asked Questions

Q1: When should I use backtracking vs dynamic programming?

A: Use backtracking when:

  • You need all solutions (not just count/optimal)
  • Problem has exponential search space that can be pruned
  • Constraints can be checked incrementally

Use dynamic programming when:

  • You need count or optimal value (not all solutions)
  • Problem has overlapping subproblems
  • Optimal substructure exists

Example:

  • All paths → Backtracking
  • Number of paths → DP
  • Shortest path → DP or BFS

Q2: How do I avoid duplicate solutions?

A: Common strategies:

  1. Sort input and skip duplicates:

    1
    2
    3
    4
    nums.sort()
    for i in range(start, n):
    if i > start and nums[i] == nums[i-1]:
    continue

  2. Use start index (for combinations):

    1
    2
    3
    # Only consider elements from start onward
    for i in range(start, n):
    backtrack(path, i + 1) # Next start is i+1

  3. Track used elements (for permutations):

    1
    2
    3
    used = [False] * n
    if used[i]:
    continue

Q3: Why is my solution too slow?

A: Check these:

  1. Not pruning early: Add constraint checks before recursion
  2. Copying unnecessarily: Only copy when saving final solutions
  3. Inefficient data structures: Use sets/arrays instead of lists for lookups
  4. Redundant work: Memoize if subproblems overlap

Example optimization:

1
2
3
4
5
6
7
# Slow: checking in O(n) list
if choice in path: # O(n) lookup
continue

# Fast: checking in O(1) set
if choice in used_set: # O(1) lookup
continue

Q4: Can I use iteration instead of recursion?

A: Yes, but recursion is usually cleaner for backtracking. Iterative approach uses an explicit stack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def iterative_backtrack():
stack = [([], initial_state)]
result = []

while stack:
path, state = stack.pop()
if is_goal(path):
result.append(path[:])
continue

for choice in get_choices(state):
if is_valid(choice, path):
new_path = path + [choice]
new_state = update_state(state, choice)
stack.append((new_path, new_state))

return result

Trade-off: Iteration avoids stack overflow but is more verbose.

Q5: How do I handle problems with multiple constraints?

A: Track all constraints separately and check them all:

1
2
3
4
5
6
7
8
9
# Example: N-Queens
used_cols = set()
used_diag1 = set()
used_diag2 = set()

def is_valid(row, col):
return (col not in used_cols and
(row - col) not in used_diag1 and
(row + col) not in used_diag2)

Q6: What's the difference between permutations and combinations?

A:

  • Permutations: Order matters. [1,2][2,1]
  • Combinations: Order doesn't matter. [1,2] = [2,1]

Implementation difference:

  • Permutations: Can choose any unused element
  • Combinations: Use start index, only consider nums[start:]

Q7: How do I optimize for "find one solution" vs "find all solutions"?

A: Return early when first solution found:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def find_one_solution():
def backtrack(path, choices):
if is_goal(path):
return path[:] # Return immediately

for choice in choices:
if not is_valid(choice, path):
continue
path.append(choice)
result = backtrack(path, updated_choices)
if result: # Found solution
return result
path.pop()
return None

Q8: Can backtracking solve optimization problems?

A: Yes, but usually DP is better. For backtracking:

1
2
3
4
5
6
7
8
9
10
11
12
best_solution = None
best_value = float('inf')

def backtrack(path, choices):
if is_goal(path):
value = evaluate(path)
if value < best_value:
best_value = value
best_solution = path[:]
return

# ... explore choices

Note: This explores all solutions. Use DP if you only need optimal value.

Q9: How do I handle problems with variable-length solutions?

A: Save solutions at multiple points, not just at base case:

1
2
3
4
5
6
7
8
# Example: Subsets - save at every step
def backtrack(path, start):
result.append(path[:]) # Save current subset

for i in range(start, n):
path.append(nums[i])
backtrack(path, i + 1)
path.pop()

Q10: What's the best way to practice backtracking?

A: 1. Start with template: Memorize the universal template 2. Solve classic problems: Permutations, combinations, subsets, N-Queens 3. Identify patterns: Notice similarities across problems 4. Focus on constraints: Practice identifying and checking constraints 5. Optimize gradually: First get it working, then add pruning

Recommended progression: 1. Permutations (basic) 2. Subsets (binary choices) 3. Combination Sum (reuse + constraints) 4. N-Queens (multiple constraints) 5. Generate Parentheses (custom constraints) 6. Word Break II (advanced, with memoization)


Summary

Backtracking is a powerful paradigm for solving constraint satisfaction problems. Here are the key takeaways:

Core Concepts

  1. Framework: Make choice → Recurse → Backtrack (undo choice)
  2. Three elements: Choices, Constraints, Goal
  3. Relationship with DFS: Backtracking uses DFS but modifies and restores state

Template Pattern

1
2
3
4
5
6
7
8
9
10
11
def backtrack(path, choices):
if is_goal(path):
result.append(path[:])
return

for choice in choices:
if not is_valid(choice, path):
continue
path.append(choice)
backtrack(path, update_choices(choices, choice))
path.pop()

Key Techniques

  1. Early pruning: Check constraints before recursing
  2. State management: Always restore state after recursion
  3. Avoid duplicates: Sort and skip, or use start index
  4. Efficient tracking: Use sets/arrays for O(1) lookups

Problem Patterns

  • Permutations: Track used elements
  • Combinations: Use start index to avoid duplicates
  • Subsets: Save at every step
  • Constraint satisfaction: Track all constraints separately
  • String generation: Track counts (like parentheses)

Complexity

  • Time: Usually exponential (or) but pruned significantly
  • Space:recursion depth +auxiliary structures

Best Practices

  1. ✅ Copy paths when saving: result.append(path[:])
  2. ✅ Always backtrack: path.pop() after recursion
  3. ✅ Check constraints early: Before making choice
  4. ✅ Use efficient data structures: Sets for membership, arrays for tracking
  5. ✅ Prune aggressively: Skip invalid paths immediately

Master backtracking by practicing the classic problems, internalizing the template, and learning to identify when backtracking is the right approach. With these tools, you'll be able to tackle a wide range of LeetCode problems efficiently!

  • Post title:LeetCode (8): Backtracking Algorithm
  • Post author:Chen Kai
  • Create time:2023-07-28 00:00:00
  • Post link:https://www.chenk.top/en/leetcode-backtracking/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments