Recursion & Backtracking
What is Recursion?
Recursion is a programming technique where a function calls itself to solve a smaller sub-problem of the original problem. Every recursive function must have two parts:
- Base Case: The condition that terminates the recursion (prevents infinite loops and stack overflow).
- Recursive Step: The logic that breaks the problem down and calls the function again.
What is Backtracking?
Backtracking is an extension of recursion. It is a “trial and error” approach used to build a solution incrementally. If the current path doesn’t lead to a valid solution, you “backtrack” (undo the last step) and try a different path.
The Backtracking Template
void Backtrack(state, result) {
if (isSolution(state)) {
result.Add(state);
return;
}
for (option in options) {
if (isValid(option)) {
MakeChoice(option); // Step 1: Forward
Backtrack(state, result); // Step 2: Recurse
UndoChoice(option); // Step 3: Backtrack (Undo)
}
}
}
Practice Exercise
- Subsets: Given an integer array, return all possible subsets.
- Permutations: Given an array of distinct integers, return all possible permutations.
- Sudoku Solver: Fill a 9x9 grid so that each row, column, and 3x3 subgrid contains digits 1-9.
Answer
1. Subsets ( Time)
public IList<IList<int>> Subsets(int[] nums) {
var result = new List<IList<int>>();
GenerateSubsets(0, nums, new List<int>(), result);
return result;
}
private void GenerateSubsets(int start, int[] nums, List<int> current, List<IList<int>> result) {
result.Add(new List<int>(current)); // Add current snapshot
for (int i = start; i < nums.Length; i++) {
current.Add(nums[i]); // Make choice
GenerateSubsets(i + 1, nums, current, result); // Recurse
current.RemoveAt(current.Count - 1); // Backtrack
}
}
2. Permutations ( Time)
public IList<IList<int>> Permute(int[] nums) {
var result = new List<IList<int>>();
Solve(nums, new List<int>(), result);
return result;
}
private void Solve(int[] nums, List<int> current, List<IList<int>> result) {
if (current.Count == nums.Length) {
result.Add(new List<int>(current));
return;
}
for (int i = 0; i < nums.Length; i++) {
if (current.Contains(nums[i])) continue; // Skip used numbers
current.Add(nums[i]);
Solve(nums, current, result);
current.RemoveAt(current.Count - 1); // Backtrack
}
}
3. Sudoku Solver ( Worst Case)
public bool SolveSudoku(char[][] board) {
for (int r = 0; r < 9; r++) {
for (int c = 0; c < 9; c++) {
if (board[r][c] == '.') {
for (char val = '1'; val <= '9'; val++) {
if (IsValid(board, r, c, val)) {
board[r][c] = val;
if (SolveSudoku(board)) return true;
board[r][c] = '.'; // Backtrack
}
}
return false; // No valid number found for this cell
}
}
}
return true;
}
private bool IsValid(char[][] board, int row, int col, char c) {
for (int i = 0; i < 9; i++) {
if (board[i][col] == c) return false; // Column check
if (board[row][i] == c) return false; // Row check
if (board[3*(row/3) + i/3][3*(col/3) + i%3] == c) return false; // Box check
}
return true;
}
Summary
- Recursion is for problems that can be solved by reducing them to identical sub-problems (like Factorial or Tree Traversal).
- Backtracking is for search problems where you need to explore a “decision tree” (like finding paths or combinations).
- Always be careful with the base case—without it, you’ll hit a
StackOverflowException.