Backtracking Algorithms
Combinations, permutations, and constraint satisfaction
Backtracking - Complete Guide
Backtracking - Complete Guide
Introduction
Backtracking is a general algorithmic technique for solving problems recursively by trying to build a solution incrementally, one piece at a time, removing those solutions that fail to satisfy the constraints of the problem at any point of time. It is a systematic way to explore all possible configurations of a problem, often used for constraint satisfaction problems.
Key Characteristics
- Recursive Exploration: It relies heavily on recursion to explore different paths.
- State Restoration: It involves "undoing" changes (backtracking) when a path leads to a dead end.
- Constraint Checking: It checks constraints at every step to prune invalid paths early.
- Exponential Time: Typically has exponential time complexity as it explores all possibilities.
Core Techniques
1. The Backtracking Loop
A fundamental pattern where you iterate through candidates, make a choice, recurse, and then undo the choice.
Key Applications:
- Solving Sudoku
- Word Search puzzles
Learn more: The Backtracking Loop โ
2. Permutations
Generating all possible arrangements of a set of distinct elements.
Key Applications:
- Next Permutation
- Permutations II (with duplicates)
Learn more: Permutations โ
3. Combinations
Selecting a subset of items from a larger set where order does not matter.
Key Applications:
- Combination Sum
- Combination Sum III
Learn more: Combinations โ
4. Subsets
Finding the power set of a given array (all possible subsets).
Key Applications:
- Subsets
- Subsets II
Learn more: Subsets โ
Code Examples
// Example: Generating Subsets (Backtracking)
function subsets(nums: number[]): number[][] {
const result: number[][] = [];
function backtrack(start: number, current: number[]) {
// Add a copy of the current subset to the result
result.push([...current]);
for (let i = start; i < nums.length; i++) {
current.push(nums[i]); // Choose
backtrack(i + 1, current); // Explore
current.pop(); // Un-choose (Backtrack)
}
}
backtrack(0, []);
return result;
}// Example: N-Queens (Constraint Satisfaction)
function solveNQueens(n: number): string[][] {
const board: string[][] = Array.from({ length: n }, () => Array(n).fill('.'));
const solutions: string[][] = [];
function isValid(board: string[][], row: number, col: number): boolean {
// Check column
for (let i = 0; i < row; i++) {
if (board[i][col] === 'Q') return false;
}
// Check upper left diagonal
for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] === 'Q') return false;
}
// Check upper right diagonal
for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] === 'Q') return false;
}
return true;
}
function backtrack(row: number) {
if (row === n) {
solutions.push(board.map(row => row.join('')));
return;
}
for (let col = 0; col < n; col++) {
if (isValid(board, row, col)) {
board[row][col] = 'Q';
backtrack(row + 1);
board[row][col] = '.';
}
}
}
backtrack(0);
return solutions;
}Time and Space Complexity Guide
- O(N!): For generating all permutations, the time complexity is factorial because there are N! ways to arrange N items.
- O(2^N): For generating all subsets, the time complexity is exponential as there are 2^N subsets.
- O(N): Space complexity is primarily determined by the recursion stack depth, which is O(N) in the worst case.
Learning Path
Beginner
- Basic Recursion concepts
- Subsets (Power Set)
- Subsets II (Handling duplicates)
Intermediate
- Permutations
- Permutations II (Handling duplicates)
- Combination Sum
Advanced
- N-Queens
- Word Search
- Sudoku Solver
- Palindrome Partitioning
Practice Strategy
- Master the State: Understand exactly what state you need to track (e.g., current index, current path, current sum).
- Visualize the Stack: Draw the recursion tree to understand how the algorithm explores and backtracks.
- Handle Duplicates: Practice identifying when to skip duplicates in loops to avoid redundant paths.
- Optimize Pruning: Learn to add early return conditions if the constraints are violated.
Common Mistakes to Avoid
- Not Copying Arrays: Modifying the array in place and then backtracking without restoring the original state (or using a copy).
- Missing Base Case: Forgetting to stop the recursion, leading to stack overflow or infinite loops.
- Incorrect Loop Bounds: Using
i++instead ofi + 1when moving to the next recursive call, causing repeated use of the same element. - Ignoring Constraints: Not checking validity before recursing, leading to invalid states deep in the call stack.
Real-World Applications
- Puzzle Solving: Solving logic puzzles like Sudoku and Crosswords.
- Game AI: Determining all possible moves in Chess or Checkers.
- Route Planning: Finding all possible paths in a graph or map.
- Configuration Management: Generating valid configurations for hardware or software setups.
Summary
Backtracking is a powerful paradigm for solving constraint satisfaction problems where the solution space is vast but constrained. While it can be computationally expensive, understanding how to prune invalid paths and manage state is essential for any software engineer preparing for technical interviews.