Skip to content

Backtracking

Backtracking is a general algorithmic technique that systematically explores all possible solutions to a problem by incrementally building candidates and abandoning a candidate (“backtracking”) as soon as it is determined that the candidate cannot possibly lead to a valid solution. It’s essentially a brute-force approach, but it cleverly prunes the search space by eliminating branches that are guaranteed to be fruitless. This makes it significantly more efficient than a completely exhaustive search in many cases.

Why is it important? Backtracking solves problems where we need to find all or some solutions that satisfy a given set of constraints. These problems often involve exploring a combinatorial space of possibilities.

Core Concepts:

  • State Space: The set of all possible configurations or partial solutions.
  • Candidate Solutions: Partial solutions being explored.
  • Constraints: Rules that must be satisfied by a valid solution.
  • Pruning: The process of eliminating branches of the search tree that cannot lead to a valid solution.

2. When to Use Backtracking (and When Not To)

Section titled “2. When to Use Backtracking (and When Not To)”

Use Backtracking when:

  • You need to find all solutions to a problem.
  • The problem can be broken down into smaller subproblems.
  • The problem involves exploring a large but finite search space.
  • Constraints can be easily checked at each step.

Don’t use Backtracking when:

  • The search space is incredibly vast and pruning doesn’t significantly reduce it. In such cases, heuristic search algorithms might be more appropriate.
  • A more efficient dynamic programming or greedy approach exists. These algorithms often avoid redundant computations that backtracking might perform.
  • The problem involves finding only one solution, and a faster algorithm (like a greedy approach) can find it.

3. Core Algorithm / Data Structure Template

Section titled “3. Core Algorithm / Data Structure Template”

The core of backtracking involves recursion. Here’s a general template:

function backtrack(current_solution, remaining_problem):
if remaining_problem is empty:
add current_solution to solutions
return
for each choice in possible_choices(remaining_problem):
if choice is valid:
add choice to current_solution
backtrack(updated_current_solution, updated_remaining_problem)
remove choice from current_solution // Backtrack!

This template highlights the key steps: exploring choices, validating them, recursively continuing, and then backtracking to explore other options. The specific implementation of possible_choices and is_valid depends heavily on the problem.

4. Code Implementations (Python, Java, C++)

Section titled “4. Code Implementations (Python, Java, C++)”

Let’s implement a classic backtracking problem: generating all permutations of a string.

def permute(s):
"""Generates all permutations of a string using backtracking."""
result = []
def backtrack(index, current_permutation):
if index == len(s):
result.append("".join(current_permutation))
return
for i in range(len(s)):
if s[i] not in current_permutation: #Constraint check
current_permutation.append(s[i])
backtrack(index + 1, current_permutation)
current_permutation.pop() #Backtrack
backtrack(0, [])
return result
print(permute("abc")) # Output: ['abc', 'acb', 'bac', 'bca', 'cab', 'cba']
import java.util.ArrayList;
import java.util.List;
public class Permutations {
public List<String> permute(String s) {
List<String> result = new ArrayList<>();
backtrack(s, 0, new StringBuilder(), result);
return result;
}
private void backtrack(String s, int index, StringBuilder currentPermutation, List<String> result) {
if (index == s.length()) {
result.add(currentPermutation.toString());
return;
}
for (int i = 0; i < s.length(); i++) {
if (currentPermutation.indexOf(String.valueOf(s.charAt(i))) == -1) { //Constraint check
currentPermutation.append(s.charAt(i));
backtrack(s, index + 1, currentPermutation, result);
currentPermutation.deleteCharAt(currentPermutation.length() - 1); //Backtrack
}
}
}
public static void main(String[] args) {
Permutations p = new Permutations();
System.out.println(p.permute("abc"));
}
}
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
vector<string> permute(string s) {
vector<string> result;
vector<bool> used(s.length(), false);
string currentPermutation = "";
function<void(int)> backtrack = [&](int index) {
if (index == s.length()) {
result.push_back(currentPermutation);
return;
}
for (int i = 0; i < s.length(); i++) {
if (!used[i]) {
used[i] = true;
currentPermutation += s[i];
backtrack(index + 1);
currentPermutation.pop_back(); //Backtrack
used[i] = false;
}
}
};
backtrack(0);
return result;
}
int main() {
vector<string> permutations = permute("abc");
for (const string& p : permutations) {
cout << p << " ";
}
cout << endl; // Output: abc acb bac bca cab cba
return 0;
}

The time and space complexity of backtracking algorithms are highly problem-dependent. They depend on the size of the search space and the effectiveness of pruning.

AlgorithmTime Complexity (Worst Case)Space Complexity (Worst Case)
String Permutation (n characters)O(n * n!)O(n) (recursive depth) + O(n!) (to store all permutations)
N-Queens (N x N board)O(N!)O(N) + O(N) (for the solution)
Subset SumO(2n) where n is the number of elementsO(n) (recursive depth) + O(2n) (to store all subsets)

Note: The worst-case scenarios assume no effective pruning. With good pruning, the actual runtime can be significantly better.

  • Effective Pruning: The key to efficient backtracking is intelligent pruning. Analyze the constraints carefully to identify conditions that allow you to eliminate entire branches of the search tree early.
  • Memoization (Dynamic Programming): If subproblems are repeatedly encountered, memoization (or dynamic programming) can dramatically improve performance by storing and reusing previously computed results.
  • Heuristics: For extremely large search spaces, incorporating heuristics to guide the search can improve performance.
  • Iterative vs. Recursive: While recursive implementations are often more elegant and easier to understand, iterative approaches can sometimes be more efficient due to avoiding function call overhead.
  • Off-by-One Errors: Carefully check your base cases and loop conditions to avoid these common errors.
  • Incorrect Constraint Checking: Ensure your constraints are correctly implemented. A bug here can lead to incorrect results or infinite loops.

Example: Letter Combinations of a Phone Number

Section titled “Example: Letter Combinations of a Phone Number”

Description: Given a string containing digits from 2-9 inclusive, return all possible letter combinations that the number could represent. A mapping of digits to letters (like on a telephone) is given.

High-Level Approach: Use backtracking to explore all possible letter combinations. The possible_choices function would map each digit to its corresponding letters. The constraint would be that we use each digit exactly once. The algorithm would recursively build combinations, adding a letter for each digit, until all digits are processed. The resulting combinations are then collected. The recursive depth would be the number of digits.