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
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.