Backtracking Pattern

1. Generate All Subsets (Powerset)

Approach Mechanism Complexity Advantage
Backtracking Recursive Include/exclude each element O(2^n * n) Natural, easy to understand
Bit Manipulation Each bit represents inclusion O(2^n * n) Iterative, no recursion stack
Total Subsets 2^n subsets for n elements Powerset size Each element: in or out
Iterative Building Add each element to existing subsets O(2^n * n) Simple iteration pattern

Example: Subsets using backtracking

function subsets(nums) {
    const result = [];
    const current = [];
    
    function backtrack(index) {
        // Add current subset to result
        result.push([...current]);
        
        // Try adding each remaining element
        for (let i = index; i < nums.length; i++) {
            current.push(nums[i]);
            backtrack(i + 1);
            current.pop(); // Backtrack
        }
    }
    
    backtrack(0);
    return result;
}

// [1,2,3] -> [[],[1],[1,2],[1,2,3],
//              [1,3],[2],[2,3],[3]]
// Time: O(2^n * n), Space: O(n) recursion

Example: Subsets using bit manipulation

function subsetsBitManipulation(nums) {
    const n = nums.length;
    const totalSubsets = 1 << n; // 2^n
    const result = [];
    
    for (let mask = 0; mask < totalSubsets; mask++) {
        const subset = [];
        
        for (let i = 0; i < n; i++) {
            // Check if i-th bit is set
            if (mask & (1 << i)) {
                subset.push(nums[i]);
            }
        }
        
        result.push(subset);
    }
    
    return result;
}

// [1,2,3] -> same result
// No recursion, iterative approach

Example: Subsets with duplicates

function subsetsWithDup(nums) {
    nums.sort((a, b) => a - b); // Sort to group duplicates
    const result = [];
    const current = [];
    
    function backtrack(index) {
        result.push([...current]);
        
        for (let i = index; i < nums.length; i++) {
            // Skip duplicates at same level
            if (i > index && nums[i] === nums[i - 1]) {
                continue;
            }
            
            current.push(nums[i]);
            backtrack(i + 1);
            current.pop();
        }
    }
    
    backtrack(0);
    return result;
}

// [1,2,2] -> [[],[1],[1,2],[1,2,2],[2],[2,2]]
// Avoids duplicate subsets like [1,2] appearing twice

2. Permutations and Next Lexicographic Order

Method Strategy Complexity Use Case
Backtracking Swap Swap elements, recurse, backtrack O(n! * n) Generate all permutations
Next Permutation Algorithm Find pivot, swap, reverse suffix O(n) per permutation Lexicographic ordering
Used Array Tracking Boolean array to mark used elements O(n) extra space Clearer logic, handles duplicates
Total Permutations n! permutations for n distinct elements Factorial growth Combinatorial explosion

Example: Permutations using backtracking

function permute(nums) {
    const result = [];
    
    function backtrack(current, remaining) {
        if (remaining.length === 0) {
            result.push([...current]);
            return;
        }
        
        for (let i = 0; i < remaining.length; i++) {
            current.push(remaining[i]);
            
            // Create new remaining array without i-th element
            const newRemaining = [
                ...remaining.slice(0, i),
                ...remaining.slice(i + 1)
            ];
            
            backtrack(current, newRemaining);
            current.pop();
        }
    }
    
    backtrack([], nums);
    return result;
}

// [1,2,3] -> [[1,2,3],[1,3,2],
//              [2,1,3],[2,3,1],
//              [3,1,2],[3,2,1]]

Example: Permutations with swap

function permuteSwap(nums) {
    const result = [];
    
    function backtrack(start) {
        if (start === nums.length) {
            result.push([...nums]);
            return;
        }
        
        for (let i = start; i < nums.length; i++) {
            // Swap current with start
            [nums[start], nums[i]] = 
                [nums[i], nums[start]];
            
            backtrack(start + 1);
            
            // Backtrack: swap back
            [nums[start], nums[i]] = 
                [nums[i], nums[start]];
        }
    }
    
    backtrack(0);
    return result;
}

// More efficient - O(1) space

Example: Next lexicographic permutation

function nextPermutation(nums) {
    // Step 1: Find rightmost pair where nums[i] < nums[i+1]
    let i = nums.length - 2;
    while (i >= 0 && nums[i] >= nums[i + 1]) {
        i--;
    }
    
    if (i >= 0) {
        // Step 2: Find rightmost element greater than nums[i]
        let j = nums.length - 1;
        while (j >= 0 && nums[j] <= nums[i]) {
            j--;
        }
        
        // Step 3: Swap
        [nums[i], nums[j]] = [nums[j], nums[i]];
    }
    
    // Step 4: Reverse suffix from i+1 to end
    let left = i + 1;
    let right = nums.length - 1;
    while (left < right) {
        [nums[left], nums[right]] = [nums[right], nums[left]];
        left++;
        right--;
    }
    
    return nums; // Modified in-place
}

// [1,2,3] -> [1,3,2]
// [3,2,1] -> [1,2,3] (wraps around)
// Time: O(n), Space: O(1)

3. N-Queens Problem Solution

Constraint Check Method Optimization Data Structure
No Same Row Place one queen per row Implicit - iterate row by row Guaranteed by iteration
No Same Column Track used columns with set/array O(1) lookup with set Set or boolean array
No Same Diagonal Track diagonals: row-col and row+col O(1) lookup with sets Two sets for both diagonals
Backtracking Try each column, recurse, undo Early pruning on conflicts O(n!) time complexity

Example: N-Queens solution

function solveNQueens(n) {
    const result = [];
    const board = Array(n).fill(null).map(() => Array(n).fill('.'));
    const cols = new Set();
    const diag1 = new Set(); // row - col
    const diag2 = new Set(); // row + col
    
    function backtrack(row) {
        if (row === n) {
            // Found valid solution
            result.push(board.map(r => r.join('')));
            return;
        }
        
        for (let col = 0; col < n; col++) {
            // Check if position is safe
            if (cols.has(col) || 
                diag1.has(row - col) || 
                diag2.has(row + col)) {
                continue;
            }
            
            // Place queen
            board[row][col] = 'Q';
            cols.add(col);
            diag1.add(row - col);
            diag2.add(row + col);
            
            backtrack(row + 1);
            
            // Backtrack
            board[row][col] = '.';
            cols.delete(col);
            diag1.delete(row - col);
            diag2.delete(row + col);
        }
    }
    
    backtrack(0);
    return result;
}

// n=4 -> [[".Q..","...Q","Q...","..Q."],
//          ["..Q.","Q...","...Q",".Q.."]]
// Time: O(n!), Space: O(n)

Example: N-Queens count only

function totalNQueens(n) {
    let count = 0;
    const cols = new Set();
    const diag1 = new Set();
    const diag2 = new Set();
    
    function backtrack(row) {
        if (row === n) {
            count++;
            return;
        }
        
        for (let col = 0; col < n; col++) {
            if (cols.has(col) || 
                diag1.has(row - col) || 
                diag2.has(row + col)) {
                continue;
            }
            
            cols.add(col);
            diag1.add(row - col);
            diag2.add(row + col);
            
            backtrack(row + 1);
            
            cols.delete(col);
            diag1.delete(row - col);
            diag2.delete(row + col);
        }
    }
    
    backtrack(0);
    return count;
}

// More efficient - no board construction

4. Sudoku Solver Backtracking

Technique Description Impact Complexity
Constraint Checking Row, column, 3x3 box validation Prune invalid choices early O(1) with precomputed sets
Most Constrained First Fill cells with fewest options first Reduces branching factor Heuristic optimization
Naked Singles Cells with only one valid option Deterministic placement Preprocessing step
Backtracking Try 1-9, recurse, undo on conflict Exhaustive search with pruning Worst case exponential

Example: Sudoku solver

function solveSudoku(board) {
    function isValid(board, row, col, num) {
        // Check row
        for (let c = 0; c < 9; c++) {
            if (board[row][c] === num) return false;
        }
        
        // Check column
        for (let r = 0; r < 9; r++) {
            if (board[r][col] === num) return false;
        }
        
        // Check 3x3 box
        const boxRow = Math.floor(row / 3) * 3;
        const boxCol = Math.floor(col / 3) * 3;
        for (let r = boxRow; r < boxRow + 3; r++) {
            for (let c = boxCol; c < boxCol + 3; c++) {
                if (board[r][c] === num) return false;
            }
        }
        
        return true;
    }
    
    function backtrack() {
        for (let row = 0; row < 9; row++) {
            for (let col = 0; col < 9; col++) {
                if (board[row][col] === '.') {
                    for (let num = 1; num <= 9; num++) {
                        const char = num.toString();
                        
                        if (isValid(board, row, col, char)) {
                            board[row][col] = char;
                            
                            if (backtrack()) {
                                return true;
                            }
                            
                            board[row][col] = '.'; // Backtrack
                        }
                    }
                    
                    return false; // No valid number found
                }
            }
        }
        
        return true; // Board complete
    }
    
    backtrack();
    return board;
}

// Modifies board in-place
// Time: O(9^m) where m = empty cells

5. Combination Sum Problem

Variant Rule Strategy Optimization
Unlimited Reuse Same element can be used multiple times Don't increment index after use Sort array, prune when exceeds target
Use Once Each element used at most once Increment index after use Skip duplicates at same level
Fixed Length K Combination must have exactly K elements Track combination size Prune when size exceeds K
Early Pruning Stop when current sum > target Requires sorted array Dramatically reduces branches

Example: Combination sum (unlimited reuse)

function combinationSum(candidates, target) {
    const result = [];
    candidates.sort((a, b) => a - b);
    
    function backtrack(start, current, sum) {
        if (sum === target) {
            result.push([...current]);
            return;
        }
        
        if (sum > target) return; // Prune
        
        for (let i = start; i < candidates.length; i++) {
            // Prune: no point continuing if exceeds
            if (sum + candidates[i] > target) break;
            
            current.push(candidates[i]);
            // Can reuse same element: start at i
            backtrack(i, current, sum + candidates[i]);
            current.pop();
        }
    }
    
    backtrack(0, [], 0);
    return result;
}

// [2,3,6,7], target=7
// -> [[2,2,3],[7]]

Example: Combination sum II (use once)

function combinationSum2(candidates, target) {
    const result = [];
    candidates.sort((a, b) => a - b);
    
    function backtrack(start, current, sum) {
        if (sum === target) {
            result.push([...current]);
            return;
        }
        
        if (sum > target) return;
        
        for (let i = start; i < candidates.length; i++) {
            // Skip duplicates at same level
            if (i > start && 
                candidates[i] === candidates[i - 1]) {
                continue;
            }
            
            if (sum + candidates[i] > target) break;
            
            current.push(candidates[i]);
            // Use once: start at i+1
            backtrack(i + 1, current, 
                     sum + candidates[i]);
            current.pop();
        }
    }
    
    backtrack(0, [], 0);
    return result;
}

// [10,1,2,7,6,1,5], target=8
// -> [[1,1,6],[1,2,5],[1,7],[2,6]]

6. Word Search in 2D Grid (DFS Backtracking)

Component Implementation Purpose Optimization
DFS from Each Cell Try starting from every position Find word starting point Early exit on first match found
Path Tracking Mark visited cells during DFS Avoid revisiting in same path Backtrack by unmarking
4-directional Search Explore up, down, left, right Find adjacent matching characters Bounds checking required
Character Matching Match current character with word[index] Validate path correctness Mismatch = immediate backtrack

Example: Word search in grid

function exist(board, word) {
    const rows = board.length;
    const cols = board[0].length;
    
    function dfs(row, col, index) {
        // Found complete word
        if (index === word.length) return true;
        
        // Out of bounds or mismatch
        if (row < 0 || row >= rows || 
            col < 0 || col >= cols ||
            board[row][col] !== word[index]) {
            return false;
        }
        
        // Mark as visited
        const temp = board[row][col];
        board[row][col] = '#';
        
        // Explore all 4 directions
        const found = 
            dfs(row + 1, col, index + 1) ||
            dfs(row - 1, col, index + 1) ||
            dfs(row, col + 1, index + 1) ||
            dfs(row, col - 1, index + 1);
        
        // Backtrack
        board[row][col] = temp;
        
        return found;
    }
    
    // Try starting from each cell
    for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
            if (dfs(r, c, 0)) return true;
        }
    }
    
    return false;
}

// board = [["A","B","C","E"],
//          ["S","F","C","S"],
//          ["A","D","E","E"]]
// word = "ABCCED" -> true
// Time: O(m*n*4^L) where L=word length

Example: Word search II (multiple words with Trie)

class TrieNode {
    constructor() {
        this.children = {};
        this.word = null;
    }
}

function findWords(board, words) {
    // Build Trie
    const root = new TrieNode();
    for (const word of words) {
        let node = root;
        for (const char of word) {
            if (!node.children[char]) {
                node.children[char] = new TrieNode();
            }
            node = node.children[char];
        }
        node.word = word;
    }
    
    const result = [];
    const rows = board.length;
    const cols = board[0].length;
    
    function dfs(row, col, node) {
        const char = board[row][col];
        if (char === '#' || !node.children[char]) return;
        
        node = node.children[char];
        
        if (node.word) {
            result.push(node.word);
            node.word = null; // Avoid duplicates
        }
        
        board[row][col] = '#';
        
        if (row > 0) dfs(row - 1, col, node);
        if (row < rows - 1) dfs(row + 1, col, node);
        if (col > 0) dfs(row, col - 1, node);
        if (col < cols - 1) dfs(row, col + 1, node);
        
        board[row][col] = char;
    }
    
    for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
            dfs(r, c, root);
        }
    }
    
    return result;
}

// More efficient for multiple words

7. Palindrome Partitioning Problem

Concept Strategy Optimization Complexity
Partition Points Try splitting at every position Only split if prefix is palindrome O(n) palindrome check per split
Backtracking Build partition, recurse, backtrack Exponential search space O(2^n) partitions possible
DP Palindrome Check Precompute all palindrome substrings O(1) palindrome lookup O(n²) preprocessing time/space
Expand Around Center Check palindrome by expanding outward O(n) per check, no extra space Trade-off: time vs space

Example: Palindrome partitioning

function partition(s) {
    const result = [];
    
    function isPalindrome(str, left, right) {
        while (left < right) {
            if (str[left] !== str[right]) return false;
            left++;
            right--;
        }
        return true;
    }
    
    function backtrack(start, current) {
        if (start === s.length) {
            result.push([...current]);
            return;
        }
        
        for (let end = start; end < s.length; end++) {
            // Only proceed if substring is palindrome
            if (isPalindrome(s, start, end)) {
                current.push(s.substring(start, end + 1));
                backtrack(end + 1, current);
                current.pop();
            }
        }
    }
    
    backtrack(0, []);
    return result;
}

// "aab" -> [["a","a","b"],["aa","b"]]
// "a" -> [["a"]]
// Time: O(n * 2^n), Space: O(n)

Example: Palindrome partitioning with DP optimization

function partitionDP(s) {
    const n = s.length;
    // DP table: dp[i][j] = true if s[i..j] is palindrome
    const dp = Array(n).fill(null).map(() => Array(n).fill(false));
    
    // Precompute all palindromes
    for (let len = 1; len <= n; len++) {
        for (let i = 0; i <= n - len; i++) {
            const j = i + len - 1;
            if (len === 1) {
                dp[i][j] = true;
            } else if (len === 2) {
                dp[i][j] = s[i] === s[j];
            } else {
                dp[i][j] = s[i] === s[j] && dp[i + 1][j - 1];
            }
        }
    }
    
    const result = [];
    
    function backtrack(start, current) {
        if (start === n) {
            result.push([...current]);
            return;
        }
        
        for (let end = start; end < n; end++) {
            // O(1) palindrome check
            if (dp[start][end]) {
                current.push(s.substring(start, end + 1));
                backtrack(end + 1, current);
                current.pop();
            }
        }
    }
    
    backtrack(0, []);
    return result;
}

// Faster palindrome checks: O(1) instead of O(n)
// Trade-off: O(n²) space for DP table

Backtracking Pattern Summary

  • Core Pattern: Choose → Explore → Unchoose (backtrack)
  • Subsets: Include/exclude decision tree, 2^n total subsets
  • Permutations: Swap-based or used-array approach, n! permutations
  • N-Queens: Constraint propagation with sets for O(1) conflict detection
  • Sudoku: Try 1-9, validate constraints, backtrack on conflict
  • Combination Sum: Sort array, prune when sum exceeds target
  • Word Search: DFS with visited marking, 4-directional exploration
  • Palindrome Partition: Try all splits, verify palindrome, recurse
Note: Backtracking is exhaustive search with pruning. Key optimizations: (1) Early termination when constraints violated, (2) Sort data for pruning opportunities, (3) Precompute expensive checks (like palindrome DP table), (4) Track state efficiently (sets for O(1) lookup vs arrays).
Warning: Backtracking has exponential time complexity in worst case. Always implement pruning strategies to reduce search space. For Sudoku/N-Queens, use sets for O(1) constraint checking instead of O(n) scans. Remember to backtrack properly - undo all state changes when returning from recursion.