Backtracking

Decision Tree | Pruning | Subsets | Permutations | N-Queens

šŸ“˜ 11 Sections 🧩 2 Solved Problems Advanced Difficulty Prerequisite: Ch8

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:

  1. CHOOSE: pick a candidate element to add to the current partial solution.
  2. EXPLORE: recurse with the candidate added.
  3. 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 Target
candidates = [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

Medium

Given 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 at index >= 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

Hard

Place 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 path by 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 use push_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 > 0 instead of i > 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 use i > 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 (not i+1) to allow picking same element again.
  • If problem only needs COUNT or OPTIMAL VALUE and subproblems overlap, prefer DP over backtracking.