Step 1 Understand it
Remember it by
Choose, explore, un-choose — and stop early when a path can't work.
Watch it happen
Read the animation as text
- Every subset is a list of yes/no choices. Start with the empty path []. Record it — every path is a subset, not just the full ones.
- Add 1: path [1]. Record it. Then try each later number: 2, 3.
- Add 2: path [1, 2]. Record it. Then try each later number: 3.
- Add 3: path [1, 2, 3]. Record it. No later numbers to add → undo the last choice and go back up.
- Add 3: path [1, 3]. Record it. No later numbers to add → undo the last choice and go back up.
- Add 2: path [2]. Record it. Then try each later number: 3.
- Add 3: path [2, 3]. Record it. No later numbers to add → undo the last choice and go back up.
- Add 3: path [3]. Record it. No later numbers to add → undo the last choice and go back up.
- All 8 subsets: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]. Choose, explore, un-choose. Only trying LATER numbers stops [2, 1] repeating [1, 2].
The idea
Build a solution one choice at a time. After each choice, explore everything that follows, then undo the choice and try the next one. It walks a tree of decisions; pruning cuts off branches that can't succeed.
Picture it
flowchart TD R["[ ]"] -->|"take 1"| A["[1]"] R -->|"skip 1"| B["[ ]"] A -->|"take 2"| C["[1, 2]"] A -->|"skip 2"| D["[1]"] B -->|"take 2"| E["[2]"] B -->|"skip 2"| F["[ ]"]
Analogy
Walking a maze with chalk. At every fork, pick a path; at a dead end, walk back to the last fork, rub out your mark and try the next path.
Where the analogy breaks
In a maze you stop at the exit. Here you usually want every solution, so you record each one and keep exploring.
More detail
What it is
Imagine trying to find your way out of a physical maze. You pick a path, and if you hit a dead end, you physically turn around, walk back to the last intersection, and try a different path. This is Backtracking! It's a brute-force method where you exhaustively try to build a solution piece by piece. The moment you realize your current path is invalid (or you've found a full answer), you 'backtrack' (undo your last step) and try another option.
When to use it
- When a problem asks for ALL valid combinations, permutations, or subsets.
- When solving puzzles with specific constraints (like Sudoku or N-Queens).
- When the input size is inherently small (N < 20).
How it works
You write a recursive function. You make a 'choice' (e.g. adding a number to a temporary list). You dive deeper by calling the recursive function. Wait for it to finish. Then, you absolutely must UNDO the choice you just made (remove the number from the list). This resets your state so you can safely try the next choice in the loop!
Example walkthrough
Input: nums = [1,2,3], find all subsets
- Start empty [].
- Add 1 -> [1]. Recurse.
- Add 2 -> [1, 2]. Recurse.
- Add 3 -> [1, 2, 3]. Recurse. (Save this solution!)
- Backtrack: remove 3 -> [1, 2].
- Backtrack: remove 2 -> [1]. Add 3 -> [1, 3]... and so on.
Result: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
Related patterns
- Depth-First Search (DFS): Backtracking is fundamentally just DFS on an imaginary tree of decisions.
Step 2 Recognise it
Words that signal it
- 'All' combinations, permutations, subsets or partitions
- 'Generate every valid …' (parentheses, palindrome splits)
- Small input (n ≤ 15–20): exponential time is expected
- Placing things under rules: N-Queens, Sudoku, word search on a grid
Don't confuse it with
- Dynamic Programming: 'How many ways' or 'the best way' with repeated subproblems → DP. 'List them all' → backtracking.
- BFS: Shortest number of steps → BFS. Every possible arrangement → backtracking.
Gut check
Do they want every possible arrangement, and n is small? → backtracking.
How to recognise it in an interview
The problem wants 'all possible combinations', 'subsets', 'permutations', or specifically mentions finding paths on a board.
Step 3 The template
The general shape. Adapt it to each problem rather than learning one solution per problem.
def backtrack_all(nums):
results, path = [], []
def backtrack(start):
results.append(path[:]) # record a COPY (or only when complete)
for i in range(start, len(nums)):
# (optional) prune: skip choices that can't work
path.append(nums[i]) # choose
backtrack(i + 1) # explore (use i to allow reuse)
path.pop() # un-choose
backtrack(0)
return resultsThe same template in Java
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(result, new ArrayList<>(), nums, 0);
return result;
}
private void backtrack(List<List<Integer>> result, List<Integer> current, int[] nums, int start) {
// Deep copy the current list and add it to our final results
result.add(new ArrayList<>(current));
for (int i = start; i < nums.length; i++) {
current.add(nums[i]); // Make a choice
backtrack(result, current, nums, i + 1); // Explore further
current.remove(current.size() - 1); // Undo the choice (Backtrack!)
}
}
}Common mistakes
- Appending path instead of path[:] (a copy): every saved answer then changes as path changes.
- Forgetting to undo the choice (path.pop()) after exploring.
- Getting duplicates: sort first and skip equal neighbours at the same depth (Subsets II, Combination Sum II).
Step 4 Watch it run
Two problems, traced step by step exactly as the code runs.
Easy example: Subsets
Input: nums = [1, 2, 3]
| path | action | then |
|---|---|---|
| [] | record [] | next choices: [1, 2, 3] |
| [1] | record [1] | next choices: [2, 3] |
| [1, 2] | record [1, 2] | next choices: [3] |
| [1, 2, 3] | record [1, 2, 3] | no more choices → go back |
| [1, 3] | record [1, 3] | no more choices → go back |
| [2] | record [2] | next choices: [3] |
| [2, 3] | record [2, 3] | no more choices → go back |
| [3] | record [3] | no more choices → go back |
Result: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
Notice: Recording at every call (not just at the leaves) is what produces all 8 subsets, including the empty one.
def subsets(nums):
results, path = [], []
def backtrack(start):
results.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i + 1)
path.pop()
backtrack(0)
return resultsMedium example: Combination Sum (numbers can be reused)
Input: candidates = [2, 3, 5], target = 8
| path | total | what happens |
|---|---|---|
| [] | 0 | try [2, 3, 5] (can reuse) |
| [2] | 2 | try [2, 3, 5] (can reuse) |
| [2, 2] | 4 | try [2, 3, 5] (can reuse) |
| [2, 2, 2] | 6 | try [2, 3, 5] (can reuse) |
| [2, 2, 2, 2] | 8 | = target → record it ✓ |
| [2, 2, 2, 3] | 9 | over target → go back ✗ |
| [2, 2, 2, 5] | 11 | over target → go back ✗ |
| [2, 2, 3] | 7 | try [3, 5] (can reuse) |
| [2, 2, 3, 3] | 10 | over target → go back ✗ |
| [2, 2, 3, 5] | 12 | over target → go back ✗ |
| [2, 2, 5] | 9 | over target → go back ✗ |
| [2, 3] | 5 | try [3, 5] (can reuse) |
| [2, 3, 3] | 8 | = target → record it ✓ |
| [2, 3, 5] | 10 | over target → go back ✗ |
| [2, 5] | 7 | try [5] (can reuse) |
| [2, 5, 5] | 12 | over target → go back ✗ |
| [3] | 3 | try [3, 5] (can reuse) |
| [3, 3] | 6 | try [3, 5] (can reuse) |
| [3, 3, 3] | 9 | over target → go back ✗ |
| [3, 3, 5] | 11 | over target → go back ✗ |
| [3, 5] | 8 | = target → record it ✓ |
| [5] | 5 | try [5] (can reuse) |
| [5, 5] | 10 | over target → go back ✗ |
Result: [[2, 2, 2, 2], [2, 3, 3], [3, 5]]
Notice: Passing i (not i + 1) lets a number be reused; starting from i (not 0) stops [2, 3, 3] and [3, 2, 3] both appearing.
def combination_sum(candidates, target):
results, path = [], []
def backtrack(start, total):
if total == target:
results.append(path[:])
return
if total > target:
return # prune
for i in range(start, len(candidates)):
path.append(candidates[i])
backtrack(i, total + candidates[i]) # i: reuse allowed
path.pop()
backtrack(0, 0)
return resultsStep 5 Practise
Read each question and say the pattern and the trick out loud before you open the answer. Then solve it on LeetCode. Tick a problem once you've solved it on your own: it comes back for review tomorrow, then after 3, 7, 14 and 30 days, then every month, on the DSA page.
Medium
-
Subsets Medium
Return every subset of a list of distinct numbers.
Show answer
Technique: Backtracking (take or skip)
The clue: 'All subsets'.
The trick: Record the path at every call; for each later number, choose it, recurse, un-choose it.
-
Combination Sum Medium
Find all combinations (numbers can be reused) that add up to a target.
Show answer
Technique: Backtracking with reuse
The clue: 'All combinations' that hit a target, reuse allowed.
The trick: Recurse with the same index to reuse a number; stop when the total passes the target.
-
Combination Sum II Medium
Find all unique combinations that add up to a target; each number used at most once, and the input has duplicates.
Show answer
Technique: Backtracking + sort + skip duplicates
The clue: 'All unique combinations' with repeated input values.
The trick: Sort; at the same depth, skip a number equal to the previous one; recurse with i + 1.
-
Permutations Medium
Return every ordering of a list of distinct numbers.
Show answer
Technique: Backtracking with a used set
The clue: 'All permutations' = every order.
The trick: At each position try every unused number; mark it used, recurse, unmark.
-
Subsets II Medium
Return every subset when the input has duplicates, with no duplicate subsets.
Show answer
Technique: Backtracking + sort + skip duplicates
The clue: 'All subsets' but the input repeats values.
The trick: Sort; skip nums[i] when it equals nums[i − 1] at the same depth (i > start).
-
Generate Parentheses Medium
Generate every valid string of n pairs of brackets.
Show answer
Technique: Backtracking with counts
The clue: 'Generate all valid' strings.
The trick: Add '(' while open < n; add ')' while close < open.
-
Word Search Medium
Decide whether a word can be traced in a letter grid by moving up, down, left or right without reusing a cell.
Show answer
Technique: Grid backtracking (DFS)
The clue: Trace a path on a grid where a cell can't be reused.
The trick: DFS from every cell, mark the cell used during the path and unmark it when returning.
-
Palindrome Partitioning Medium
Split a string into pieces so that every piece is a palindrome; return all such splits.
Show answer
Technique: Backtracking on cut positions
The clue: 'All ways to split'.
The trick: Try every end position for the next piece; only recurse when that piece is a palindrome.
-
Return every letter string a phone keypad number could stand for.
Show answer
Technique: Backtracking over digits
The clue: 'All combinations', one choice per digit.
The trick: For each digit, try each of its letters and recurse to the next digit.
Hard
-
N Queens Hard
Place n queens on an n × n board so that none attack each other; return all boards.
Show answer
Technique: Backtracking row by row with sets
The clue: Placing items under rules, all solutions.
The trick: Place one queen per row; track used columns and both diagonals (r − c and r + c) in sets.
What to say in the interview
'Because we need all combinations, the time complexity will naturally be exponential. I will use backtracking to explore the state-space tree.'
Follow-up prep
How do you avoid duplicate subsets if the input array has duplicate numbers?