Classic Backtracking Problems
TL;DR
Classic backtracking problems establish patterns applicable to any constraint satisfaction challenge. N-Queens: Place N queens on board with no attacks; constraint is safe placement checking; decision is column selection per row. Sudoku: Fill grid with digits 1-9 respecting row/column/box constraints; decision is digit selection per cell; constraint is uniqueness checking. Permutations: Generate ordered arrangements of n elements; decision is which element to pick next; no constraints (explore full tree). Combinations: Generate unordered selections of k from n elements; decision is include/exclude element; constraint is count tracking. Subsets: Generate all 2^n subsets; binary decision tree (include or exclude each element); no constraints. Sudoku shown here demonstrates complex multi-constraint satisfaction; optimize with digit frequency analysis and bit manipulation for large problems.
Core Concepts
N-Queens Analysis
Problem Statement: Place N queens on N×N board such no two queens attack (same row, column, or diagonal).
Key Insight: One queen per row means row is implicitly solved. Only need to track column and diagonal conflicts.
Constraint Checking: Two queens at (r1, c1) and (r2, c2) conflict if:
- Same column: c1 == c2
- Same diagonal: |r1 - r2| == |c1 - c2|
Optimization: Use sets or bitmasks to track occupied columns/diagonals. Check in O(1) instead of O(n).
Sudoku Solver Strategy
Problem State: 9×9 grid with some cells filled; fill remaining cells with digits 1-9.
Constraints: Each digit appears once per row, column, and 3×3 box.
Optimization Strategies:
- Find cell with minimum possible values (Most Constrained Variable heuristic)
- Track possible digits per cell using sets or bitmasks
- Eliminate candidates when digit placed in same row/column/box
Time Complexity: Best case O(n) with perfect heuristics; worst case O(9^n) where n = empty cells.
Permutations vs Combinations vs Subsets
Permutations: Order matters. n! results for n elements. Decision: which element to pick next.
Combinations: Order doesn't matter. C(n,k) = n!/(k!(n-k)!) results. Decision: include/exclude element, but respect ordering to avoid duplicates.
Subsets: Include any number of elements. 2^n results. Binary tree structure.
Key Difference: Permutations pick all n elements every time; combinations pick k elements; subsets can have any size. Avoid duplicates by maintaining indices (next choice must be > previous choice).
Backtracking with Constraints
Constraint Types:
- Hard Constraints: Must be satisfied (N-Queens attacks, Sudoku digit uniqueness)
- Soft Constraints: Preferred but not required (minimize steps, prefer certain values)
Checking Strategy: Always check before recursing. If constraint fails, don't recurse.
Pruning Benefit: One invalid partial solution eliminates entire subtree. In Sudoku, placing invalid digit immediately fails; pruning can eliminate millions of branches.
Key Algorithms
Sudoku Solver Pseudo-code
function solveSudoku(grid):
for each cell in grid:
if cell is empty:
for each digit 1-9:
if digit is valid in cell (not in row/col/box):
place digit
if solveSudoku(grid):
return true
remove digit (backtrack)
return false (no valid digit found)
return true (all cells filled)
Permutations with Optimization
Swap-based approach: Swap elements in-place, swap back on backtrack. Avoids copying arrays.
Combination Constraint
To avoid duplicates, track starting index. Next choice must be >= current index to maintain order.
Practical Example
- Python
- Java
- TypeScript
# Sudoku Solver
def solve_sudoku(board):
"""
Solve Sudoku puzzle in-place.
board: 9x9 list of lists, 0 represents empty cell
"""
def is_valid(row, col, digit):
"""Check if digit can be placed at (row, col)"""
# Check row
if digit in board[row]:
return False
# Check column
if digit in [board[i][col] for i in range(9)]:
return False
# Check 3x3 box
box_row, box_col = 3 * (row // 3), 3 * (col // 3)
for i in range(box_row, box_row + 3):
for j in range(box_col, box_col + 3):
if board[i][j] == digit:
return False
return True
def backtrack():
"""Try to fill empty cells"""
for row in range(9):
for col in range(9):
if board[row][col] == 0:
# Try digits 1-9
for digit in range(1, 10):
if is_valid(row, col, digit):
board[row][col] = digit
if backtrack():
return True
# Backtrack
board[row][col] = 0
return False # No valid digit found
return True # All cells filled
backtrack()
return board
# Permutations
def permute(nums):
"""Generate all permutations of nums"""
result = []
def backtrack(path):
if len(path) == len(nums):
result.append(path[:])
return
for i in range(len(nums)):
if nums[i] not in path:
path.append(nums[i])
backtrack(path)
path.pop()
backtrack([])
return result
# Combinations
def combine(n, k):
"""Generate all combinations of k elements from 1..n"""
result = []
def backtrack(start, current):
if len(current) == k:
result.append(current[:])
return
for i in range(start, n + 1):
current.append(i)
backtrack(i + 1, current)
current.pop()
backtrack(1, [])
return result
# Tests
sudoku_board = [
[5,3,0,0,7,0,0,0,0],
[6,0,0,1,9,5,0,0,0],
[0,9,8,0,0,0,0,6,0],
[8,0,0,0,6,0,0,0,3],
[4,0,0,8,0,3,0,0,1],
[7,0,0,0,2,0,0,0,6],
[0,6,0,0,0,0,2,8,0],
[0,0,0,4,1,9,0,0,5],
[0,0,0,0,8,0,0,7,9]
]
solve_sudoku(sudoku_board)
print("Sudoku solved:")
for row in sudoku_board:
print(row)
print("\nPermutations of [1,2,3]:")
perms = permute([1, 2, 3])
print(f"Count: {len(perms)}")
print("\nCombinations C(4,2):")
combs = combine(4, 2)
print(combs)
public class ClassicBacktracking {
// Sudoku Solver
public static boolean solveSudoku(char[][] board) {
for (int row = 0; row < 9; row++) {
for (int col = 0; col < 9; col++) {
if (board[row][col] == '.') {
for (char digit = '1'; digit <= '9'; digit++) {
if (isValid(board, row, col, digit)) {
board[row][col] = digit;
if (solveSudoku(board)) {
return true;
}
board[row][col] = '.';
}
}
return false;
}
}
}
return true;
}
private static boolean isValid(char[][] board, int row, int col, char digit) {
// Check row
for (int c = 0; c < 9; c++) {
if (board[row][c] == digit) return false;
}
// Check column
for (int r = 0; r < 9; r++) {
if (board[r][col] == digit) return false;
}
// Check 3x3 box
int boxRow = 3 * (row / 3);
int boxCol = 3 * (col / 3);
for (int r = boxRow; r < boxRow + 3; r++) {
for (int c = boxCol; c < boxCol + 3; c++) {
if (board[r][c] == digit) return false;
}
}
return true;
}
// Permutations
public static List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrackPermute(nums, new ArrayList<>(), new boolean[nums.length], result);
return result;
}
private static void backtrackPermute(int[] nums, List<Integer> path,
boolean[] used, List<List<Integer>> result) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (!used[i]) {
path.add(nums[i]);
used[i] = true;
backtrackPermute(nums, path, used, result);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
// Combinations
public static List<List<Integer>> combine(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
backtrackCombine(n, k, 1, new ArrayList<>(), result);
return result;
}
private static void backtrackCombine(int n, int k, int start,
List<Integer> path,
List<List<Integer>> result) {
if (path.size() == k) {
result.add(new ArrayList<>(path));
return;
}
for (int i = start; i <= n; i++) {
path.add(i);
backtrackCombine(n, k, i + 1, path, result);
path.remove(path.size() - 1);
}
}
public static void main(String[] args) {
System.out.println("Permutations of [1,2,3]:");
List<List<Integer>> perms = permute(new int[]{1, 2, 3});
System.out.println("Count: " + perms.size());
System.out.println("\nCombinations C(4,2):");
List<List<Integer>> combs = combine(4, 2);
System.out.println(combs);
}
}
// Sudoku Solver
function solveSudoku(board: string[][]): boolean {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (board[row][col] === '.') {
for (let digit = '1'; digit <= '9'; digit++) {
if (isValid(board, row, col, digit)) {
board[row][col] = digit;
if (solveSudoku(board)) {
return true;
}
board[row][col] = '.';
}
}
return false;
}
}
}
return true;
}
function isValid(board: string[][], row: number, col: number, digit: string): boolean {
// Check row
for (let c = 0; c < 9; c++) {
if (board[row][c] === digit) return false;
}
// Check column
for (let r = 0; r < 9; r++) {
if (board[r][col] === digit) return false;
}
// Check 3x3 box
const boxRow = 3 * Math.floor(row / 3);
const boxCol = 3 * Math.floor(col / 3);
for (let r = boxRow; r < boxRow + 3; r++) {
for (let c = boxCol; c < boxCol + 3; c++) {
if (board[r][c] === digit) return false;
}
}
return true;
}
// Permutations
function permute(nums: number[]): number[][] {
const result: number[][] = [];
const used = Array(nums.length).fill(false);
function backtrack(path: number[]): void {
if (path.length === nums.length) {
result.push([...path]);
return;
}
for (let i = 0; i < nums.length; i++) {
if (!used[i]) {
path.push(nums[i]);
used[i] = true;
backtrack(path);
path.pop();
used[i] = false;
}
}
}
backtrack([]);
return result;
}
// Combinations
function combine(n: number, k: number): number[][] {
const result: number[][] = [];
function backtrack(start: number, path: number[]): void {
if (path.length === k) {
result.push([...path]);
return;
}
for (let i = start; i <= n; i++) {
path.push(i);
backtrack(i + 1, path);
path.pop();
}
}
backtrack(1, []);
return result;
}
// Tests
console.log("Permutations of [1,2,3]:");
const perms = permute([1, 2, 3]);
console.log(`Count: ${perms.length}`);
console.log("\nCombinations C(4,2):");
const combs = combine(4, 2);
console.log(combs);
Common Pitfalls
Sudoku digit elimination errors: When placing digit, forgetting to update possible values for remaining cells. Leads to inefficient search or missed optimizations.
Permutations with duplicates: Not using visited array or similar mechanism. Same element picked multiple times or duplicates in result.
Combinations allowing duplicates: Forgetting to maintain ordering constraint (next choice > previous choice). Generates [1,2] and [2,1] as different combinations.
Inefficient Sudoku checking: Scanning entire row/column/box each time. Pre-compute sets or use bit manipulation for O(1) validation.
Not finding optimal heuristic: Sudoku solver picks cells in arbitrary order. Picking cell with fewest possibilities (Most Constrained Variable) can reduce search space exponentially.
Returning early vs collecting all solutions: For Sudoku, return as soon as found. For permutations/combinations, continue exploring to collect all. Make sure logic matches intent.
Self-Check
- In Sudoku, why is checking constraints before recursing important? What happens if you only check after completion?
- Why do permutations require a visited array but combinations only require a starting index?
- How would you modify the Sudoku solver to find the first solution in minimum time?
One Takeaway
Classic problems demonstrate backtracking patterns: constraint satisfaction (Sudoku), exhaustive generation (permutations/combinations), and pruning effectiveness (N-Queens). The breakthrough is recognizing problem structure: Is it CSP (Sudoku), exhaustive generation (perms), or counting (combinations)? Choose algorithms accordingly. For each pattern, implement constraint checking rigorously and pruning intelligently—this transforms exponential search into practical solutions.
References
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press.
- Kleinberg, J., & Tardos, É. (2005). Algorithm Design. Pearson Education.