Back to Home
Backtracking
Backtracking is an algorithmic technique for solving problems recursively by trying to build a solution incrementally and abandoning solutions that fail to satisfy constraints.
When to Use
- Problems asking for all possible solutions
- Constraint satisfaction problems
- Combinatorial problems (permutations, combinations)
- Puzzle solving (Sudoku, N-Queens)
Common Patterns
Decision Space
Make decisions at each step, backtrack if invalid
Example: Generate parentheses, letter combinations
Permutations
Generate all orderings of elements
Example: Permutations, permutations II
Subsets/Combinations
Choose subset of elements
Example: Subsets, combination sum
Code Templates
Python
# General backtracking template
def backtrack(result, current, choices, constraints):
# Base case: solution is complete
if is_solution(current):
result.append(current.copy())
return
# Try each choice
for choice in choices:
# Make choice
if is_valid(choice, current, constraints):
current.append(choice)
# Recurse
backtrack(result, current, choices, constraints)
# Undo choice (backtrack)
current.pop()
# Permutations template
def permute(nums):
result = []
def backtrack(current, remaining):
if not remaining:
result.append(current[:])
return
for i in range(len(remaining)):
current.append(remaining[i])
backtrack(current, remaining[:i] + remaining[i+1:])
current.pop()
backtrack([], nums)
return result
# Subsets template
def subsets(nums):
result = []
def backtrack(start, current):
result.append(current[:])
for i in range(start, len(nums)):
current.append(nums[i])
backtrack(i + 1, current)
current.pop()
backtrack(0, [])
return resultC++
// General backtracking template
void backtrack(vector<vector<int>>& result,
vector<int>& current,
vector<int>& choices,
Constraints& constraints) {
// Base case: solution is complete
if (isSolution(current)) {
result.push_back(current);
return;
}
// Try each choice
for (int choice : choices) {
// Make choice
if (isValid(choice, current, constraints)) {
current.push_back(choice);
// Recurse
backtrack(result, current, choices, constraints);
// Undo choice (backtrack)
current.pop_back();
}
}
}
// Permutations template
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> result;
function<void(vector<int>&, vector<int>&)> backtrack =
[&](vector<int>& current, vector<int>& remaining) {
if (remaining.empty()) {
result.push_back(current);
return;
}
for (int i = 0; i < remaining.size(); i++) {
current.push_back(remaining[i]);
vector<int> next = remaining;
next.erase(next.begin() + i);
backtrack(current, next);
current.pop_back();
}
};
vector<int> empty;
backtrack(empty, nums);
return result;
}
// Subsets template
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
vector<int> current;
function<void(int)> backtrack = [&](int start) {
result.push_back(current);
for (int i = start; i < nums.size(); i++) {
current.push_back(nums[i]);
backtrack(i + 1);
current.pop_back();
}
};
backtrack(0);
return result;
}Practice Problems
Common Pitfalls
- ⚠️Modifying shared state without backtracking
- ⚠️Forgetting to copy current solution when adding to result
- ⚠️Not pruning invalid branches early enough
- ⚠️Incorrect base case detection
Complexity
Time
Often O(2^n) or O(n!) depending on problem
Space
O(n) for recursion depth
Exponential but with pruning can be practical