Backtracking is a class of algorithms for finding solutions to computational problems, particularly constraint satisfaction and enumeration problems. It works by:
Incrementally building candidates to the solution
Abandoning candidates (“backtracking”) as soon as it determines they cannot possibly be completed to a valid solution
“Backtracking is like exploring a maze - you try a path, and if it leads nowhere, you return to the last decision point and try another.”
Real-World Analogy
Scenario
Backtracking Equivalent
Solving a maze
Try path → Hit dead end → Return to fork → Try next path
Sudoku solver
Place number → Check conflicts → Backtrack → Try next number
Auto-correct
Generate word → Check dictionary → Backtrack → Try different letters
When to Use Backtracking
Suitable problems:
Generate all valid combinations/subsets/permutations
voidbacktrack(int startIndex, List<Integer> path, List<List<Integer>> res) { // 1. Base case: when to stop? if (终止条件) { res.add(newArrayList<>(path)); return; }
// 2. Loop through choices for (inti= startIndex; i < n; i++) { // 3. Choose path.add(nums[i]);
Branch: Different options at the same level (alternatives)
Understanding startIndex
startIndex controls where the current level begins searching.
Scenario
startIndex Behavior
Combinations
i + 1 - start from next element
Permutations
Always 0 - can use any unused element
Subsets
i + 1 - expand search range
What i + 1 Really Means
1 2 3 4
If we pick element at index i: - Current level: uses nums[i] - Next level: starts from index i + 1 - Result: avoids reusing nums[i] or earlier elements
Why this prevents duplicates:
Branch 1 (start=0): picks nums[0]=1 → then search range [1,2,3…]
Branch 2 (start=1): picks nums[1]=2 → then search range [2,3…]
Branch 3 (start=2): picks nums[2]=3 → then search range [3…]
Result: [1,2] and [2,1] never both appear
Pruning: Necessity, Not Optimization
Early Termination Logic
Pruning eliminates paths that cannot possibly succeed:
1 2 3 4 5
Example: Choose k elements from n Remaining elements needed: k - path.size() Remaining elements available: n - i + 1
If (n - i + 1 < k - path.size()) → impossible to succeed → skip
The constraint: i <= n - (k - path.size()) + 1
Variable
Meaning
n
Total elements available
k
Target number of elements
path.size()
Elements already chosen
n - i + 1
Elements remaining (including current)
Pruning in practice
1 2 3 4 5 6 7 8 9 10 11 12 13
// Without pruning - explores unnecessary branches for (inti= startIndex; i <= n; i++) { path.add(nums[i]); backtrack(i + 1, path, res); path.remove(path.size() - 1); }
// With pruning - skips impossible branches for (inti= startIndex; i <= n - (k - path.size()) + 1; i++) { path.add(nums[i]); backtrack(i + 1, path, res); path.remove(path.size() - 1); }
Deduplication Strategy
Why Deduplication Matters
Without deduplication, problems with duplicate values produce duplicate results:
Input
Without Dedup
With Dedup
[1,1,2]
[[1,1,2],[1,2,1],[1,1,2],[1,2,1],…]
[[1,1,2],[1,2,1]]
The i > startIndex Pattern
1
if (i > startIndex && nums[i] == nums[i - 1]) continue;
Correct interpretation:
i > startIndex: We’re at the same tree level (same decision depth)
nums[i] == nums[i-1]: Same value as previous branch
Meaning: Avoid picking the same value twice within the same level
What it does NOT mean:
❌ “No duplicates allowed in the input”
❌ “No duplicates allowed in the result”
From Easy to Hard: Problem Progression
Combinations
Problem: Choose k elements from n, order doesn’t matter
Key constraint: startIndex + i + 1
Template:
1 2 3 4 5 6 7 8 9
voidcombine(int n, int k) { backtrack(1, n, k, newArrayList<>(), result); }
for (inti= startIndex; i <= n - (k - path.size()) + 1; i++) { path.add(i); backtrack(i + 1, n, k, path, res); path.remove(path.size() - 1); }
Subsets
Problem: Generate all possible subsets (the power set)
Key difference: Collect result at every step, not just leaf nodes
classSolution { public List<List<Integer>> combine(int n, int k) { List<List<Integer>> res = newArrayList<>(); backtrack(1, n, k, newArrayList<>(), res); return res; }
privatevoidbacktrack(int startIndex, int n, int k, List<Integer> path, List<List<Integer>> res) { if (path.size() == k) { res.add(newArrayList<>(path)); return; } for (inti= startIndex; i <= n - (k - path.size()) + 1; i++) { path.add(i); backtrack(i + 1, n, k, path, res); path.remove(path.size() - 1); } } }
classSolution { public List<List<String>> solveNQueens(int n) { List<List<String>> res = newArrayList<>(); char[][] board = newchar[n][n]; for (char[] row : board) Arrays.fill('.'); boolean[] cols = newboolean[n]; boolean[] diag1 = newboolean[2 * n - 1]; boolean[] diag2 = newboolean[2 * n - 1]; backtrack(0, n, board, cols, diag1, diag2, res); return res; }
privatevoidbacktrack(int row, int n, char[][] board, boolean[] cols, boolean[] diag1, boolean[] diag2, List<List<String>> res) { if (row == n) { List<String> solution = newArrayList<>(); for (char[] r : board) solution.add(newString(r)); res.add(solution); return; } for (intcol=0; col < n; col++) { if (cols[col] || diag1[row - col + n - 1] || diag2[row + col]) continue; board[row][col] = 'Q'; cols[col] = true; diag1[row - col + n - 1] = true; diag2[row + col] = true; backtrack(row + 1, n, board, cols, diag1, diag2, res); board[row][col] = '.'; cols[col] = false; diag1[row - col + n - 1] = false; diag2[row + col] = false; } } }
Tree Traversal for n=4 (2 results):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Row 0 ├── Col 0 (Q at 0,0) │ ├── Col 2 (Q at 1,2) - diagonal conflict, skip │ └── Col 3 (Q at 1,3) - diagonal conflict, skip │ → No solution from this branch ├── Col 1 (Q at 0,1) │ ├── Col 3 (Q at 1,3) │ │ ├── Col 0 (Q at 2,0) │ │ │ └── Col 2 (Q at 3,2) → Valid! [".Q..","...Q","Q...","..Q."] │ └── Col 0 (Q at 1,0) │ └── ... (symmetric to above) ├── Col 2 (Q at 0,2) - symmetric to Col 1 └── Col 3 (Q at 0,3) - symmetric to Col 0 Total: 2 solutions
Result Count by n:
n
Solutions
1
1
2
0
3
0
4
2
5
10
6
4
7
40
8
92
9. [131] Palindrome Partitioning - Medium
Problem: Partition a string such that every substring is a palindrome.