Backtracking
Decision Tree | Pruning | Subsets | Permutations | N-Queens
1 ā What Is Backtracking?
Backtracking is a systematic method for exploring all possible solutions to a problem by building candidates incrementally and abandoning (pruning) a candidate as soon as it is determined that it cannot lead to a valid solution. It is a refined form of brute-force that avoids redundant exploration.
The Backtracking Template
Every backtracking solution follows the same skeleton:
- CHOOSE: pick a candidate element to add to the current partial solution.
- EXPLORE: recurse with the candidate added.
- UN-CHOOSE (backtrack): undo the choice before trying the next candidate.
The recursion tree is called the 'decision tree'. Each node is a partial state; each edge is a choice. Leaves are complete solutions or dead ends. Pruning cuts entire subtrees early: if no solution can exist in this subtree (constraint violated), skip it without exploring.
// Universal Backtracking Template
void backtrack(State& state, vector<Result>& results) {
// Base case: complete solution found
if (isComplete(state)) {
results.push_back(buildResult(state));
return;
}
// Try every candidate for the next choice
for (auto& candidate : getCandidates(state)) {
if (!isValid(state, candidate)) continue; // prune invalid
makeChoice(state, candidate); // CHOOSE
backtrack(state, results); // EXPLORE
undoChoice(state, candidate); // UN-CHOOSE
}
}1.1 ā Backtracking vs Brute Force vs DP
| Dimension | Brute Force | Backtracking | Dynamic Programming |
|---|---|---|---|
| Exploration | All possible states | Prune invalid subtrees early | Reuse stored subproblem results |
| When used | No known structure | Constraint-based search | Overlapping subproblems |
| Undo step | Not needed | Required (un-choose) | Not needed |
| Complexity | Worst case exponential | Exponential but pruned | Polynomial (with memoisation) |
| Output type | One optimal value | All valid solutions (or one) | One optimal value |
| Space | O(1) extra | O(depth) recursion stack | O(n) to O(n²) table |
| Classic problems | Loop over all subsets | Subsets, Permutations, N-Queens | Coin Change, LCS, Knapsack |
Real-World Analogy: Solving a Maze
You stand at the entrance of a maze and want to find the exit. At each junction, you try one path. If you hit a dead end, you backtrack to the last junction and try a different path. This is exactly backtracking: explore a path fully, and if it fails, undo your steps and try the next option.
Pruning: if a corridor is blocked (invalid constraint), skip it immediately without entering.
The maze metaphor maps to code: junction = recursive call, dead end = base case failure, backtrack = undo the last choice.
2 ā Visual Diagrams: Decision Trees
Diagram 1 ā Subsets Decision Tree
Subsets: Full Decision Tree (n=3)nums = [1, 2, 3]
Generate all subsets (power set). At each level, we decide: include nums[i] or skip it.
[]
/ \
[1] []
/ \ / \
[1,2] [1] [2] []
/ \ / \ / \ / \
[1,2,3][1,2][1,3][1][2,3][2][3][]
Leaves (all 8 = 2³ subsets): [1,2,3] [1,2] [1,3] [1] [2,3] [2] [3] []
No pruning needed here (all paths are valid). Total nodes in tree = 2^(n+1) - 1 = 15 for n=3.
Diagram 2 ā Permutations Decision Tree
Permutations: Decision Tree (n=3)nums = [1, 2, 3]
Generate all permutations. At each level, pick one unused number. used = {} tracks which numbers are already in the current path.
[]
/ | \
[1] [2] [3]
/ \ / \ / \
[1,2][1,3][2,1][2,3][3,1][3,2]
| | | | | |
[1,2,3][1,3,2][2,1,3][2,3,1][3,1,2][3,2,1]
6 leaves = 3! = n! permutations. No pruning (no duplicates in input). Total nodes = 1 + 3 + 6 + 6 = 16 for n=3.
With duplicates (e.g. [1,1,2]): sort first, then skip if nums[i] == nums[i-1] and nums[i-1] was NOT used in this level. This prunes duplicate branches at each depth level.
Diagram 3 ā N-Queens Pruning
N-Queens: Constraint-Based Pruning
N=4: place 4 queens on a 4x4 board, no two attacking each other. Place one queen per row. For each row, try all columns.
Row 0: try col 0, 1, 2, 3. Row 0, col 0: Q . . . Row 1, try cols: col 0 (same col, PRUNE), col 1 (diagonal, PRUNE), col 2 (safe), col 3 (diagonal, PRUNE). Row 1, col 2: Q . . . . . Q . Row 2, try cols: all attacked by Q at (0,0) or (1,2) -> all PRUNED. Backtrack to row 1. Try col 3: Q . . . . . . Q Row 2, col 1: Q . . . . . . Q . Q . . Row 3: cols 0 (col prune), 1 (col prune), 2 (diag prune), 3 (col prune). All pruned. Backtrack to row 0, try col 1. ...
Solutions found: 2 (for N=4).
. Q . . . . Q . . . . Q Q . . . Q . . . . . . Q . . Q . . Q . .
Pruning criteria: same column, same diagonal (r1-c1 == r2-c2), or same anti-diagonal (r1+c1 == r2+c2).
Diagram 4 ā Combination Sum
Combination Sum: Pruning on Remaining Targetcandidates = [2, 3, 6, 7], target = 7
Find all combinations that sum to target (reuse allowed).
start=0 (index), path=[], remaining=7 | +-- pick 2, remaining=5 | +-- pick 2, remaining=3 | | +-- pick 2, remaining=1 | | | +-- pick 2, remaining=-1 PRUNE (negative) | | | +-- pick 3, remaining=-2 PRUNE | | +-- pick 3, remaining=0 SOLUTION: [2,2,3] | | +-- pick 6, remaining=-3 PRUNE | +-- pick 3, remaining=2 | | +-- pick 3, remaining=-1 PRUNE | | (no more valid picks) | +-- pick 6, remaining=-1 PRUNE +-- pick 3, remaining=4 | +-- pick 3, remaining=1 | | +-- pick 3, remaining=-2 PRUNE | +-- pick 6, remaining=-2 PRUNE +-- pick 6, remaining=1 | +-- pick 6, remaining=-5 PRUNE +-- pick 7, remaining=0 SOLUTION: [7]
Solutions: [2,2,3] and [7].
3 ā Real-World Use Cases
| Problem | Backtracking Application | Industry System |
|---|---|---|
| Puzzle solving | Sudoku, crosswords, constraint satisfaction | Game engines, puzzle generators |
| Circuit layout | VLSI routing ā place wires avoiding conflicts | EDA (Electronic Design Automation) tools |
| Regex matching | NFA simulation backtracks on failed matches | grep, database query engines, parsers |
| Natural language parsing | Earley/CYK parser explores grammar rules | NLP compilers, syntax highlighters |
| Test case generation | Enumerate all input combinations for coverage | Automated software testing frameworks |
| Scheduling | Assign tasks to slots satisfying constraints | University timetabling, exam scheduling |
| Cryptography | Key space enumeration for brute-force attacks | Security penetration testing tools |
| Combinatorial optimisation | TSP branch-and-bound with backtracking pruning | Logistics, route planning, supply chain |
| AI game playing | Minimax with alpha-beta pruning | Chess engines, Go AI (pre-neural era) |
4 ā Core Concepts & Algorithms
4.1 ā Subsets (Power Set)
Generate all 2^n subsets of nums. The start index prevents permutations of the same subset.
// LeetCode 78 ā Subsets ā O(2^n * n) Time, O(n) Space
class Solution {
void bt(vector<int>& nums, int start, vector<int>& path, vector<vector<int>>& res) {
res.push_back(path); // every node is a valid subset
for (int i = start; i < (int)nums.size(); i++) {
path.push_back(nums[i]); // CHOOSE
bt(nums, i+1, path, res); // EXPLORE (i+1: no reuse)
path.pop_back(); // UN-CHOOSE
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
vector<int> path;
bt(nums, 0, path, res);
return res;
}
};
// Subsets II ā with duplicates (LC 90)
// Sort first, then skip nums[i] == nums[i-1] at the same recursion level.
void btII(vector<int>& nums, int start, vector<int>& path, vector<vector<int>>& res) {
res.push_back(path);
for (int i = start; i < (int)nums.size(); i++) {
if (i > start && nums[i] == nums[i-1]) continue; // skip duplicate
path.push_back(nums[i]);
btII(nums, i+1, path, res);
path.pop_back();
}
}4.2 ā Permutations
Order matters. We use a used[] array to track selections and loop from 0 every time.
// LeetCode 46 ā Permutations (no duplicates) ā O(n! * n) Time, O(n) Space
class Solution {
void bt(vector<int>& nums, vector<bool>& used, vector<int>& path, vector<vector<int>>& res) {
if ((int)path.size() == (int)nums.size()) {
res.push_back(path);
return;
}
for (int i = 0; i < (int)nums.size(); i++) {
if (used[i]) continue; // already in path
used[i] = true; // CHOOSE
path.push_back(nums[i]);
bt(nums, used, path, res); // EXPLORE
path.pop_back(); // UN-CHOOSE
used[i] = false;
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
vector<bool> used(nums.size(), false);
vector<int> path;
bt(nums, used, path, res);
return res;
}
};
// Permutations II ā with duplicates (LC 47)
// Sort first. Skip nums[i]==nums[i-1] when nums[i-1] is NOT used.
// (This means we only take the first copy of a duplicate at each level.)
void btII(vector<int>& nums, vector<bool>& used, vector<int>& path, vector<vector<int>>& res) {
if ((int)path.size() == (int)nums.size()) { res.push_back(path); return; }
for (int i = 0; i < (int)nums.size(); i++) {
if (used[i]) continue;
if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue; // prune dup
used[i] = true;
path.push_back(nums[i]);
btII(nums, used, path, res);
path.pop_back();
used[i] = false;
}
}4.3 ā Combinations
// LeetCode 77 ā Combinations: choose k numbers from [1..n] ā O(C(n,k) * k)
class Solution {
void bt(int n, int k, int start, vector<int>& path, vector<vector<int>>& res) {
if ((int)path.size() == k) {
res.push_back(path);
return;
}
// Pruning: need k-path.size() more elements, at most n-i+1 remain
for (int i = start; i <= n - (k - (int)path.size()) + 1; i++) {
path.push_back(i); // CHOOSE
bt(n, k, i+1, path, res); // EXPLORE
path.pop_back(); // UN-CHOOSE
}
}
public:
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> res;
vector<int> path;
bt(n, k, 1, path, res);
return res;
}
};4.4 ā Combination Sum (Reuse Allowed)
// LeetCode 39 ā Combination Sum ā Time: O(N^(T/M)) N=candidates, T=target, M=min
class Solution {
void bt(vector<int>& cands, int start, int remain, vector<int>& path, vector<vector<int>>& res) {
if (remain == 0) { res.push_back(path); return; }
for (int i = start; i < (int)cands.size(); i++) {
if (cands[i] > remain) break; // sorted: all further are larger, PRUNE
path.push_back(cands[i]);
bt(cands, i, remain - cands[i], path, res); // i (not i+1): reuse ok
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // sort to enable pruning
vector<vector<int>> res;
vector<int> path;
bt(candidates, 0, target, path, res);
return res;
}
};4.5 ā N-Queens
// LeetCode 51 ā N-Queens ā O(N!) Time, O(N) Space
// Place N queens on NĆN board so none attack each other.
class Solution {
vector<vector<string>> res;
vector<bool> col, diag1, diag2; // col, '/' diagonal, '\' diagonal
void bt(int row, int n, vector<string>& board) {
if (row == n) {
res.push_back(board);
return;
}
for (int c = 0; c < n; c++) {
// Prune: column or either diagonal is occupied
if (col[c] || diag1[row-c+n-1] || diag2[row+c]) continue;
board[row][c] = 'Q'; // CHOOSE
col[c] = diag1[row-c+n-1] = diag2[row+c] = true;
bt(row+1, n, board); // EXPLORE
board[row][c] = '.'; // UN-CHOOSE
col[c] = diag1[row-c+n-1] = diag2[row+c] = false;
}
}
public:
vector<vector<string>> solveNQueens(int n) {
col.assign(n,false);
diag1.assign(2*n-1,false); // '/' diagonals: indexed by row-col+n-1
diag2.assign(2*n-1,false); // '\' diagonals: indexed by row+col
vector<string> board(n, string(n,'.'));
bt(0, n, board);
return res;
}
};4.6 ā Word Search on Grid
// LeetCode 79 ā Word Search ā O(M*N * 4^L)
class Solution {
bool bt(vector<vector<char>>& g, string& w, int idx, int r, int c) {
if (idx == (int)w.size()) return true; // all chars matched
if (r<0||r>=(int)g.size()||c<0||c>=(int)g[0].size()) return false;
if (g[r][c] != w[idx]) return false; // mismatch: prune
char tmp = g[r][c];
g[r][c] = '#'; // CHOOSE: mark visited
bool found = bt(g,w,idx+1,r+1,c) || bt(g,w,idx+1,r-1,c) ||
bt(g,w,idx+1,r,c+1) || bt(g,w,idx+1,r,c-1);
g[r][c] = tmp; // UN-CHOOSE: restore cell
return found;
}
public:
bool exist(vector<vector<char>>& board, string word) {
for (int r=0; r<(int)board.size(); r++)
for (int c=0; c<(int)board[0].size(); c++)
if (bt(board, word, 0, r, c)) return true;
return false;
}
};5 ā Pattern Recognition Guide
| Problem Type | Template Variation | Key Decisions |
|---|---|---|
| Subsets (no duplicates) | Collect at every node; loop from start | start index prevents reuse / duplicates |
| Subsets (with duplicates) | Sort + skip nums[i]==nums[i-1] at same level | i > start guards same-level skip |
| Permutations (no dupes) | used[] array; loop from 0 every time | used[] prevents reusing same element |
| Permutations (with dupes) | Sort + skip when prev duplicate unused | !used[i-1] ensures canonical ordering |
| Combinations (k of n) | Collect when path.size()==k | Upper bound prune: i <= n-(k-path.size())+1 |
| Combination sum (reuse) | Pass i (not i+1) to allow reuse | Sort + break when candidate > remain |
| Combination sum II | Pass i+1; skip duplicates at same level | i > start && nums[i]==nums[i-1] |
| N-Queens / Sudoku | Boolean arrays for constraints | col[], diag1[], diag2[] for O(1) check |
| Word search / grid | Mark cell visited; restore on backtrack | g[r][c]='#' then restore to tmp |
| Palindrome partitioning | Collect when index == s.size() | isPalindrome check before recursing |
| Generate parentheses | Track open and close counts | open < n to add '('; close < open to add ')' |
Backtracking Complexity Formula
- For subsets: O(2^n * n) ā 2^n subsets, each copied in O(n).
- For permutations: O(n! * n) ā n! permutations, each copied in O(n).
- For combinations C(n,k): O(C(n,k) * k) ā C(n,k) results, each copied in O(k).
- For N-Queens: O(N!) with pruning significantly reducing the constant factor.
- For word search: O(M*N * 4^L) ā M*N starting points, 4^L paths of length L.
Backtracking is exponential by nature. Pruning reduces the constant but not the exponent. If a problem has overlapping subproblems AND only needs the count or optimal value (not all solutions), DP is almost always faster.
Duplicate Handling Cheat Sheet
- SUBSETS with duplicates: sort nums. In the loop, skip if
i > start && nums[i] == nums[i-1]. - PERMUTATIONS with duplicates: sort nums. Skip if
i > 0 && nums[i] == nums[i-1] && !used[i-1].
Why different?
Subsets: 'start' is the left boundary of the current level.
Permutations: level always starts at 0, so check !used[i-1] to detect same-level duplicate.
Golden rule: sort the input first, then skip consecutive duplicates AT THE SAME RECURSION LEVEL.
6 ā Complete C++ Implementations
6.1 ā Generate Parentheses
// LeetCode 22 ā Generate Parentheses ā O(4^n / sqrt(n)) Catalan number
class Solution {
void bt(int n, int open, int close, string& curr, vector<string>& res) {
if ((int)curr.size() == 2*n) { res.push_back(curr); return; }
if (open < n) { // can add '('
curr.push_back('(');
bt(n, open+1, close, curr, res);
curr.pop_back();
}
if (close < open) { // can add ')' only if open > close
curr.push_back(')');
bt(n, open, close+1, curr, res);
curr.pop_back();
}
}
public:
vector<string> generateParenthesis(int n) {
vector<string> res; string curr;
bt(n, 0, 0, curr, res);
return res;
}
};6.2 ā Palindrome Partitioning
// LeetCode 131 ā Palindrome Partitioning ā O(2^n * n) Time, O(n) Space
class Solution {
bool isPalin(string& s, int l, int r) {
while (l < r) if (s[l++] != s[r--]) return false;
return true;
}
void bt(string& s, int start, vector<string>& path, vector<vector<string>>& res) {
if (start == (int)s.size()) { res.push_back(path); return; }
for (int end = start; end < (int)s.size(); end++) {
if (!isPalin(s, start, end)) continue; // prune non-palindromes
path.push_back(s.substr(start, end-start+1));
bt(s, end+1, path, res);
path.pop_back();
}
}
public:
vector<vector<string>> partition(string s) {
vector<vector<string>> res; vector<string> path;
bt(s, 0, path, res);
return res;
}
};6.3 ā Sudoku Solver
// LeetCode 37 ā Sudoku Solver ā O(9^M) Time M=empty cells, O(M) Space
class Solution {
bool isValid(vector<vector<char>>& b, int r, int c, char d) {
for (int i=0;i<9;i++) {
if (b[r][i]==d || b[i][c]==d) return false;
if (b[3*(r/3)+i/3][3*(c/3)+i%3]==d) return false;
}
return true;
}
bool bt(vector<vector<char>>& b) {
for (int r=0;r<9;r++) {
for (int c=0;c<9;c++) {
if (b[r][c] != '.') continue;
for (char d='1'; d<='9'; d++) {
if (!isValid(b,r,c,d)) continue;
b[r][c] = d;
if (bt(b)) return true;
b[r][c] = '.';
}
return false; // no digit worked: backtrack
}
}
return true; // all cells filled
}
public:
void solveSudoku(vector<vector<char>>& board) {
bt(board);
}
};7 ā Complexity Reference
| Algorithm | Time (without pruning) | Space |
|---|---|---|
| Subsets (no duplicates) | O(2^n * n) | O(n) |
| Subsets II (duplicates) | O(2^n * n) pruning reduces const | O(n) |
| Permutations (no duplicates) | O(n! * n) | O(n) |
| Permutations II (duplicates) | O(n! * n) pruning reduces const | O(n) |
| Combinations C(n,k) | O(C(n,k) * k) | O(k) |
| Combination Sum (reuse) | O(N^(T/M)) T=target, M=min cand | O(T/M) |
| N-Queens | O(N!) heavily pruned in practice | O(N) |
| Sudoku Solver | O(9^M) M = empty cells | O(M) |
| Word Search | O(M*N * 4^L) L = word length | O(L) |
| Generate Parentheses | O(4^n / sqrt(n)) Catalan number | O(n) |
Backtracking is always exponential in the worst case ā this is unavoidable for NP problems. The stated complexity is without pruning. With good pruning, practical performance can be orders of magnitude better. Space is O(depth of recursion tree) = O(n) for most problems ā only the current path is stored on the stack.
8 ā Solved Problem 1
Combination Sum
MediumGiven an array of distinct integers candidates and a target integer target, return all unique combinations of candidates where the chosen numbers sum to target. The same number may be chosen from candidates an unlimited number of times.
Observations
Since elements can be reused, this is not a standard subset problem. We pass the same index i (not i+1) to allow reuse.
- Key insight 1: Sort candidates first. If the current candidate exceeds the remaining target, all further candidates (which are larger) also exceed it ā break early.
- Key insight 2: Use a start index to avoid generating duplicates like
[2,3]and[3,2]. By only considering candidates atindex >= start, we ensure combinations are in non-decreasing order.
Complexities
- Time: O(N^(T/M)) branching T/M deep where T=target, M=min element.
- Space: O(T/M) maximum recursion stack depth.
Dry Run (candidates = [2,3,6,7], target = 7)
Call start remain path Action
bt(0,7) 0 7 [] try 2,3,6,7
bt(0,5) 0 5 [2] try 2,3,6,7
bt(0,3) 0 3 [2,2] try 2,3,6,7
bt(0,1) 0 1 [2,2,2] try 2(>1 no), 3(break)
bt(1,1) 1 1 [2,2,3] 3>1, break. backtrack back to [2,2]
1 3 [2,2] pick 3: remain=0
remain==0 ā 0 [2,2,3] SOLUTION! add to res
Final result: [[2,2,3], [7]]
9 ā Solved Problem 2
N-Queens
HardPlace n queens on an n x n chessboard so that no two queens attack each other (no shared row, column, or diagonal). Return all distinct solutions as board configurations.
Observations
- Key insight 1: Place exactly one queen per row. This reduces the problem to choosing one column per row.
- Key insight 2: Three O(1) lookup arrays suffice for constraint checking:
col[],diag1[](indexed by row-col+n-1 for the '/' diagonal),diag2[](indexed by row+col for the '\' diagonal). - Pruning is critical: Without it, complexity is n^n. With column+diagonal pruning, the search space shrinks to approximtely n!. For n=8: 8^8 = 16M vs 8! = 40K.
Complexities
- Time: O(N!) ā search space pruned heavily.
- Space: O(n) for the recursion stack and O(1) arrays.
10 ā Common Mistakes & Edge Cases
10.1 ā Structural Mistakes
- Forgetting the un-choose (backtrack) step. Without undoing the choice, the path accumulates garbage from previous branches.
- Passing
pathby value instead of by reference. This copies the path at every node ā O(n) per call ā making the algorithm significantly slower. Always pass by reference and usepush_back/pop_back. - For subsets, collecting results only at the leaf. Every node is a valid subset ā collect at the beginning of every call.
- For combination sum with reuse, passing i+1 instead of i. This prevents reusing the same element and misses valid combinations.
10.2 ā Duplicate Handling Mistakes
- Subsets/Combo sum: Skipping duplicates using
i > 0instead ofi > start. This skips valid paths where a duplicate appears deeper in the tree, not just at the same level. - Permutations: Skipping when
!used[i-1]without sorting first. The deduplication logic only works if identical elements are adjacent. - Confusing the skip conditions: Subsets use
i > start. Permutations usei > 0 && nums[i]==nums[i-1] && !used[i-1].
10.3 ā Edge Cases
- Empty input: subsets of
[]=[[]](one empty subset). Always initialise result with empty and handle gracefully. - Single element: subsets of
[1]=[[], [1]]. Permutations of[1]=[[1]]. - Target = 0 for combination sum: the only solution is
[](empty combination).
11 ā Common Interview Questions
| Problem | Key Implementation Detail |
|---|---|
| Subsets (LC 78) | Collect at every node, loop from start |
| Subsets II (LC 90) | Sort + skip i > start && nums[i]==nums[i-1] |
| Permutations (LC 46) | used[] array, loop 0 to N every time |
| Permutations II (LC 47) | Sort + skip when !used[i-1] |
| Combination Sum (LC 39) | Reuse allowed, sort + break prune, pass i |
| Combination Sum II (LC 40) | No reuse, pass i+1, skip duplicates i > start |
| N-Queens (LC 51) | col[], diag1[], diag2[] O(1) checks |
| Word Search (LC 79) | Mark visited with '#', restore on backtrack |
| Palindrome Partitioning (LC 131) | Check palindrome before recursing to prune |
Chapter 9 ā Key Takeaways
- Backtracking =
choose+explore+un-choose. The un-choose step is non-negotiable. - Pruning is the difference between TLE and AC. Always prune invalid branches before recursing.
- Subsets: collect at EVERY node. Use start index.
- Permutations: collect at leaves only. Use
used[]array. Loop from 0. - Combination sum with reuse: pass
i(noti+1) to allow picking same element again. - If problem only needs COUNT or OPTIMAL VALUE and subproblems overlap, prefer DP over backtracking.