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:
DFS (Depth-First Search)
- 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: All
queens 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
16def 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 | def backtrack_template(): |
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:
-
Problem Analysis
Permutations problem requires generating all possible orderings of
array elements. For
Core Challenges:
- Avoiding duplicate selections: Each element can appear only once in each permutation
- Systematically exploring all possibilities: Need to try all possible permutation combinations
- 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
usedarray) - Goal: Path length equals array length (complete permutation generated)
Algorithm Flow:
- Use
usedarray to mark which numbers have been used - Use
pathlist to record current path (partial permutation) - Recursive function
backtrack():- If
pathlength equalsnumslength, 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
- If
Complexity Analysis
Time Complexity:path array:used array:
Complexity Proof:
For permutation of
Total permutations:
Solution
1 | from typing import List |
Alternative using set:
1 | def permute_set(nums): |
Complexity:
- Time:
- There are
permutations, and each takes to 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:
-
Problem Analysis
Combination Sum requires finding all combinations that sum to target, with each number reusable unlimited times.
Core Challenges:
- Avoiding duplicate combinations:
[2,3]and[3,2]are the same combination - Controlling search space: Early termination when remaining value becomes negative
- 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
startposition) - Constraints:
- Combination sum cannot exceed
target(remain >= 0) - Only choose from current position onward (avoid duplicates)
- Combination sum cannot exceed
- Goal: Combination sum equals
target(remain == 0)
Algorithm Flow:
- Define backtrack function
backtrack(start, remain):start: Starting index for current choicesremain: Remaining target sum
- Termination conditions:
remain == 0: Found valid combination, save resultremain < 0: Current path invalid, return immediately
- Iterate through all candidates starting from
start:- Add to current path
- Recurse:
backtrack(i, remain - candidates[i])(start fromi, allow reuse) - Backtrack: Remove current choice
Complexity Analysis
Time Complexity:
Space Complexity:target (when always selecting smallest
number) - path array length at most target
Complexity Proof:
Assuming minimum candidate is
Solution
1 | from typing import List |
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
- After selecting 2,
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 | # ❌ Wrong |
Pitfall 2: Using
i+1 prevents reuse
1 | # ❌ Wrong |
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:
-
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 | def subsets(nums): |
Alternative: binary decision approach:
1 | def subsets_binary(nums): |
Complexity:
- Time:
- subsets, each copied in - Space:
- Recursion depth
LeetCode 51: N-Queens
Problem Statement
The n-queens puzzle is the problem of placing
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: All
queens placed
Key insight:
- Place queens row by row (one per row)
- Track used columns and diagonals
- Diagonals:
row - col(main) androw + col(anti-diagonal) are constant
Solution
1 | def solveNQueens(n): |
Optimization: Using boolean arrays instead of sets
(faster for small
1 | def solveNQueens_optimized(n): |
Complexity:
- Time:
- First row has
choices, second has at most , etc. - Space:
- Board storage +
recursion depth
LeetCode 22: Generate Parentheses
Problem Statement
Given
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'('
- Can only add
- Goal: Length equals
and string is valid
Key insight: Track counts of open and close
parentheses. Only add ')' when there are unmatched
'('.
Solution
1 | def generateParenthesis(n): |
Alternative using string concatenation:
1 | def generateParenthesis_string(n): |
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 | # Example: Combination Sum |
2. Constraint Propagation
Use constraints to reduce choices:
1 | # Example: N-Queens |
3. Memoization (for overlapping subproblems)
Cache results of subproblems:
1 | # Example: Word Break II (advanced) |
4. Sort and Skip Duplicates
Sort input and skip duplicate choices:
1 | # Example: Combination Sum II (with duplicates) |
5. Bounds Checking
Check if remaining choices can satisfy constraints:
1 | # Example: Subset Sum |
Complexity Analysis
Understanding backtracking complexity is crucial for interviews:
Time Complexity
General formula:
-
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:
Total: Usually
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
3path.append(choice)
backtrack(path, choices)
# Missing: path.pop()
Correct: 1
2
3path.append(choice)
backtrack(path, choices)
path.pop() # Always restore state!
Pitfall 3: Modifying Choices Incorrectly
Wrong (for permutations): 1
2
3choices.remove(choice) # Modifies original list
backtrack(path, choices)
choices.append(choice) # Hard to restore
Correct: 1
2
3used[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
3path.append(choice)
if not is_valid(path): # Too late!
return
Correct: 1
2
3if not is_valid(choice, path):
continue # Skip before adding
path.append(choice)
Debugging Checklist
- ✅ Are you copying paths when saving solutions?
- ✅ Are you restoring state after recursion?
- ✅ Are constraints checked before making choices?
- ✅ Are you pruning invalid paths early?
- ✅ Is the base case correct?
- ✅ 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:
Sort input and skip duplicates:
1
2
3
4nums.sort()
for i in range(start, n):
if i > start and nums[i] == nums[i-1]:
continueUse 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+1Track used elements (for permutations):
1
2
3used = [False] * n
if used[i]:
continue
Q3: Why is my solution too slow?
A: Check these:
- Not pruning early: Add constraint checks before recursion
- Copying unnecessarily: Only copy when saving final solutions
- Inefficient data structures: Use sets/arrays instead of lists for lookups
- 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 | def iterative_backtrack(): |
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 | # Example: N-Queens |
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
startindex, only considernums[start:]
Q7: How do I optimize for "find one solution" vs "find all solutions"?
A: Return early when first solution found:
1 | def find_one_solution(): |
Q8: Can backtracking solve optimization problems?
A: Yes, but usually DP is better. For backtracking:
1 | best_solution = None |
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 | # Example: Subsets - save at every step |
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
- Framework: Make choice → Recurse → Backtrack (undo choice)
- Three elements: Choices, Constraints, Goal
- Relationship with DFS: Backtracking uses DFS but modifies and restores state
Template Pattern
1 | def backtrack(path, choices): |
Key Techniques
- Early pruning: Check constraints before recursing
- State management: Always restore state after recursion
- Avoid duplicates: Sort and skip, or use start index
- 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
- ✅ Copy paths when saving:
result.append(path[:]) - ✅ Always backtrack:
path.pop()after recursion - ✅ Check constraints early: Before making choice
- ✅ Use efficient data structures: Sets for membership, arrays for tracking
- ✅ 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.