Backtracking is a method used to solve problems by building potential solutions step by step. If it becomes clear that a partial solution cannot lead to a valid final solution, the process "backtracks" by undoing the last step and trying a different path. This approach is commonly applied to constraint satisfaction problems, combinatorial optimization, and puzzles like N-Queens or Sudoku, where all possibilities need to be explored systematically while avoiding unnecessary computations.
Backtracking explores choices depth first and rejects a branch as soon as its partial solution violates a constraint. Undoing the last choice restores the state for the next candidate. Early rejection reduces wasted exploration, though the worst-case search can still be exponential.
A good mental model is: choose → test → continue or undo. The “test” part is what turns random exploration into an algorithm with structure.
Recursive functions are functions that call themselves directly or indirectly to solve a problem by breaking it down into smaller, more manageable subproblems. This concept is fundamental in computer science and mathematics, as it allows for elegant solutions to complex problems through repeated application of a simple process.
Recursion shows up here because backtracking naturally creates “nested decisions.” Every time you make a choice, you enter a smaller version of the same problem: “Okay, given what I’ve chosen so far, what choices can I make next?” Recursion fits that pattern perfectly. The big win is that recursion keeps the code focused on the current step, while the call stack quietly remembers how you got there, so when you backtrack, you return to the exact point where the last decision was made.
Each recursive call needs a base case or a demonstrable move toward one. Without that progress, calls continue until a resource limit is reached.
Main idea:
A useful way to keep recursion readable is to ask two questions while writing it:
If you can answer those cleanly, you usually get clean code.
Recursion closely relates to mathematical induction, where a problem is solved by assuming that the solution to a smaller instance of the problem is known and building upon it.
A recursive function can often be expressed using a recurrence relation:
$$ f(n) = \begin{cases} g(n) & \text{if } n = \text{base case} \\ h(f(n - 1), n) & \text{otherwise} \end{cases} $$
where:
This is where recursion becomes more than a programming trick, it becomes a way to prove correctness. Induction and recursion share the same shape: handle the simplest case, then show how everything else reduces to it. If you ever want confidence that a recursive solution is correct, that structure is what you lean on.
The factorial of a non-negative integer $n$ is the product of all positive integers less than or equal to $n$. Mathematically, it is defined as:
$$ n! = \begin{cases} 1 & \text{if } n = 0 \\ n \times (n - 1)! & \text{if } n > 0 \end{cases} $$
Python Implementation:
def factorial(n):
if n < 0:
raise ValueError("n must be non-negative")
if n == 0:
return 1 # Base case: 0! = 1
else:
return n * factorial(n - 1) # Recursive case
Factorial is the “hello world” of recursion because it’s easy to see the shrinking problem: factorial(n) becomes factorial(n-1). And it also shows the two phases you always get with recursion: going down (making calls) and coming back up (combining results). Returning through the call stack is called unwinding. It is useful preparation for backtracking, but factorial is not a backtracking search: it does not choose among alternatives or undo tentative decisions.
Let's trace the recursive calls for factorial(5):
factorial(5) and compute $5 \times factorial(4)$ because $5 \neq 0$.factorial(4) and compute $4 \times factorial(3)$.factorial(3) and compute $3 \times factorial(2)$.factorial(2) and compute $2 \times factorial(1)$.factorial(1) and compute $1 \times factorial(0)$.factorial(0) and return $1$ as the base case is reached.Now the calls unwind and compute the results:
factorial(1) returns $1 \times 1 = 1$.factorial(2) returns $2 \times 1 = 2$.factorial(3) returns $3 \times 2 = 6$.factorial(4) returns $4 \times 6 = 24$.factorial(5) returns $5 \times 24 = 120$.Thus, $5! = 120$.
The downward phase creates pending multiplications; the return phase completes them. Backtracking uses the same call-stack behavior, with the additional work of undoing a choice and trying another candidate.
Each recursive call can be visualized as a node in a tree:
factorial(5)
|
+-- factorial(4)
|
+-- factorial(3)
|
+-- factorial(2)
|
+-- factorial(1)
|
+-- factorial(0)
The leaves represent the base case, and the tree unwinds as each recursive call returns.
Important Considerations:
These points matter even more once you move from “mathy recursion” (like factorial) to “search recursion” (like backtracking). Search trees can get deep and wide. That’s why good backtracking code doesn’t just recurse, it also prunes aggressively and keeps state management clean, so you aren’t blowing up time or memory.
Depth-First Search is an algorithm for traversing or searching tree or graph data structures. It starts at a selected node and explores as far as possible along each branch before backtracking.
DFS is the bridge between “recursion as a technique” and “backtracking as a strategy.” DFS says: go deep first, then rewind. That rewind is literally the same behavior: when you hit a dead end, you return to the last decision point and try a different neighbor. If you’ve ever solved a maze by walking forward until you can’t, then returning to the last intersection, that’s DFS in human form.
Main idea:
A quick “do and don’t” that makes DFS feel less abstract:
Pseudocode:
DFS(node):
mark node as visited
for each neighbor in node.neighbors:
if neighbor is not visited:
DFS(neighbor)
Consider the following tree:
Tree:
A
/ \
B C
/ \
D E
Traversal using DFS starting from node 'A':
Traversal order: $A → B → C → D → E$
Implementation in Python:
class Node:
def __init__(self, value):
self.value = value
self.children = []
self.visited = False
def dfs(node):
node.visited = True
print(node.value)
for child in node.children:
if not child.visited:
dfs(child)
# Create nodes
node_a = Node('A')
node_b = Node('B')
node_c = Node('C')
node_d = Node('D')
node_e = Node('E')
# Build the tree
node_a.children = [node_b, node_c]
node_c.children = [node_d, node_e]
# Perform DFS
dfs(node_a)
Analysis:
If you care about performance, the key takeaway is: DFS is often cheap enough to be a default traversal tool, but it can still get expensive in huge graphs. Also, that visited flag is doing a lot of work, without it, graphs with cycles can turn DFS into an accidental infinite adventure.
Backtracking is an algorithmic technique for solving problems recursively by trying to build a solution incrementally, removing solutions that fail to satisfy the constraints at any point.
At this point, you can think of backtracking as DFS with standards. DFS explores; backtracking explores while enforcing rules and cutting off bad paths early. That’s why people use it for puzzles and constraint problems: you don’t just want to wander, you want to wander with a checklist, so you can confidently say “this can’t possibly work” and move on fast.
Main Idea:
The “prune the search space” is the reason backtracking is usable. Without pruning, N-Queens becomes “try everything,” and “everything” grows ridiculously fast. With pruning, you still explore possibilities, but you stop feeding time into branches that are already doomed.
General Template (pseudocode)
function backtrack(partial):
if is_complete(partial):
handle_solution(partial)
return
for candidate in generate_candidates(partial):
if is_valid(candidate, partial):
place(candidate, partial) // extend partial with candidate
backtrack(partial)
unplace(candidate, partial) // undo extension (backtrack)
Pieces you supply per problem:
is_complete: does partial represent a full solution?handle_solution: record/output the solution.generate_candidates: possible next choices given current partial.is_valid: pruning test to reject infeasible choices early.place / unplace: apply and revert the choice.The beauty of this template is that it teaches you what backtracking really is: not one specific algorithm, but a reusable shape. Most backtracking problems differ only in those helper functions. If you can clearly define “what counts as valid so far,” you can solve a lot of classic puzzles with the same skeleton.
Python-ish Generic Framework
def backtrack(partial, is_complete, generate_candidates, is_valid, handle_solution):
if is_complete(partial):
handle_solution(partial)
return
for candidate in generate_candidates(partial):
if not is_valid(candidate, partial):
continue
# make move
partial.append(candidate)
backtrack(partial, is_complete, generate_candidates, is_valid, handle_solution)
# undo move
partial.pop()
You can wrap those callbacks into a class or closures for stateful problems. Returning from a complete solution still allows the caller to try the next candidate, so this template enumerates all solutions. To stop at the first solution, return a success flag through every call. If handle_solution stores a mutable list, store a copy; otherwise later undo operations alter the recorded result.
One practical “do/don’t” with this style:
place/unplace symmetrical (every change you make must be undone).The N-Queens problem is a classic puzzle in which the goal is to place $N$ queens on an $N \times N$ chessboard such that no two queens threaten each other. In chess, a queen can move any number of squares along a row, column, or diagonal. Therefore, no two queens can share the same row, column, or diagonal.
Objective:
N-Queens is the poster child for backtracking because it’s easy to describe, hard to brute force, and perfect for pruning. The moment you place a queen, you instantly rule out a bunch of squares. That means you don’t need to “wait until the end” to discover failure, you can detect it as soon as it happens, which is exactly what backtracking wants.
To better understand the problem, let's visualize it using ASCII graphics.
Empty $4 \times 4$ Chessboard:
0 1 2 3 (Columns)
+---+---+---+---+
0| | | | |
+---+---+---+---+
1| | | | |
+---+---+---+---+
2| | | | |
+---+---+---+---+
3| | | | |
+---+---+---+---+
(Rows)
Each cell can be identified by its row and column indices ((row, column)).
Example Solution for $N = 4$:
One of the possible solutions for placing 4 queens on a $4 \times 4$ chessboard is:
0 1 2 3 (Columns)
+---+---+---+---+
0| | Q | | | (Queen at position (0, 1))
+---+---+---+---+
1| | | | Q | (Queen at position (1, 3))
+---+---+---+---+
2| Q | | | | (Queen at position (2, 0))
+---+---+---+---+
3| | | Q | | (Queen at position (3, 2))
+---+---+---+---+
(Rows)
Q represents a queen.Backtracking is an ideal algorithmic approach for solving the N-Queens problem due to its constraint satisfaction nature. The algorithm incrementally builds the solution and backtracks when a partial solution violates the constraints.
High-Level Steps:
This flow is exactly “choose → test → recurse → undo.” The fun part is that the board doesn’t need to be fully drawn most of the time. You can represent a queen placement compactly (like “row -> column”), and then your safety check becomes pure logic. That’s a nice lesson: backtracking is often more about managing state than about fancy data structures.
Below is a Python implementation of the N-Queens problem using backtracking.
def solve_n_queens(N):
if N < 0:
raise ValueError("N must be non-negative")
solutions = []
board = [-1] * N # board[row] = column position of queen in that row
def is_safe(row, col):
for prev_row in range(row):
# Check column conflict
if board[prev_row] == col:
return False
# Check diagonal conflicts
if abs(board[prev_row] - col) == abs(prev_row - row):
return False
return True
def place_queen(row):
if row == N:
# All queens are placed successfully
solutions.append(board.copy())
return
for col in range(N):
if is_safe(row, col):
board[row] = col # Place queen
place_queen(row + 1) # Move to next row
board[row] = -1 # Backtrack
place_queen(0)
return solutions
# Example usage
N = 4
solutions = solve_n_queens(N)
print(f"Number of solutions for N={N}: {len(solutions)}")
for index, sol in enumerate(solutions):
print(f"\nSolution {index + 1}:")
for row in range(N):
line = ['.'] * N
if sol[row] != -1:
line[sol[row]] = 'Q'
print(' '.join(line))
0 to N - 1.1.There are two distinct solutions for $N = 4$:
Solution 1:
Board Representation: [1, 3, 0, 2]
0 1 2 3
+---+---+---+---+
0| | Q | | |
+---+---+---+---+
1| | | | Q |
+---+---+---+---+
2| Q | | | |
+---+---+---+---+
3| | | Q | |
+---+---+---+---+
Solution 2:
Board Representation: [2, 0, 3, 1]
0 1 2 3
+---+---+---+---+
0| | | Q | |
+---+---+---+---+
1| Q | | | |
+---+---+---+---+
2| | | | Q |
+---+---+---+---+
3| | Q | | |
+---+---+---+---+
Number of solutions for N=4: 2
Solution 1:
. Q . .
. . . Q
Q . . .
. . Q .
Solution 2:
. . Q .
Q . . .
. . . Q
. Q . .
The algorithm explores the solution space as a tree, where each node represents a partial solution (queens placed up to a certain row). The branches represent the possible positions for the next queen.
The backtracking occurs when a node has no valid branches (no safe positions in the next row), prompting the algorithm to return to the previous node and try other options.
I. The search has factorial growth because valid partial placements use distinct columns. The displayed implementation also tests every column and scans earlier rows in is_safe. A conservative bound is $O(N^2 N!)$, plus $O(SN)$ to copy $S$ solutions. Quoting $O(N!)$ alone omits this implementation's candidate-checking cost. Column and diagonal sets can make each conflict test constant time.
II. Auxiliary space is $O(N)$, excluding the $O(SN)$ stored output, where:
board array stores the positions of the $N$ queens.This is a good moment to connect the “why should I care?” dot: many real problems look like N-Queens under the hood, scheduling, assignment, routing with constraints, configuration systems, even some parts of compiler design. You’re practicing a general way to search through choices without drowning in them.
Given a maze represented as a 2D grid, find a path from the starting point to the goal using backtracking. The maze consists of open paths and walls, and movement is allowed in four directions: up, down, left, and right (no diagonal moves). The goal is to determine a sequence of moves that leads from the start to the goal without crossing any walls.
Maze solving makes backtracking feel instantly real. You’re making choices (directions), you hit walls or loops (constraints), and you undo moves when you get stuck. Even if you never care about mazes, you do care about the pattern: this is the same structure as exploring possible decisions in a game AI, navigating states in a program, or searching combinations until you find one that works.
Grid Cells:
. (dot) represents an open path.# (hash) represents a wall or obstacle.Allowed Moves:
Let's visualize the maze using ASCII graphics to better understand the problem.
Maze Layout:
Start (S) at position (0, 0)
Goal (G) at position (5, 5)
0 1 2 3 4 5 (Columns)
0 S . # . . .
1 . # . . . .
2 . . . . # .
3 . # # # . .
4 . . . # . .
5 # # # # . G
Legend:
S - Start
G - Goal
. - Open path
# - Wall
Here's the maze grid with indices:
0 1 2 3 4 5
+---+---+---+---+---+---+
0 | S | . | # | . | . | . |
+---+---+---+---+---+---+
1 | . | # | . | . | . | . |
+---+---+---+---+---+---+
2 | . | . | . | . | # | . |
+---+---+---+---+---+---+
3 | . | # | # | # | . | . |
+---+---+---+---+---+---+
4 | . | . | . | # | . | . |
+---+---+---+---+---+---+
5 | # | # | # | # | . | G |
+---+---+---+---+---+---+
Objective:
Find a sequence of moves from S to G, navigating only through open paths (.) and avoiding walls (#). The path should be returned as a list of grid coordinates representing the steps from the start to the goal.
def solve_maze(maze, start, goal):
if not maze or not maze[0]:
return None
rows, cols = len(maze), len(maze[0])
path = []
def is_valid(x, y):
return (0 <= x < rows and 0 <= y < cols and maze[x][y] == '.')
def explore(x, y):
if not is_valid(x, y):
return False
if (x, y) == goal:
path.append((x, y))
return True
maze[x][y] = 'V' # Mark as visited
path.append((x, y))
# Try all possible directions: down, up, right, left
if (explore(x + 1, y) or
explore(x - 1, y) or
explore(x, y + 1) or
explore(x, y - 1)):
maze[x][y] = '.'
return True
path.pop() # Backtrack
maze[x][y] = '.' # Unmark visited
return False
if explore(*start):
return path
else:
return None
# Sample maze (as a list of lists)
maze = [
['.', '.', '#', '.', '.', '.'],
['.', '#', '.', '.', '.', '.'],
['.', '.', '.', '.', '#', '.'],
['.', '#', '#', '#', '.', '.'],
['.', '.', '.', '#', '.', '.'],
['#', '#', '#', '#', '.', '.']
]
start = (0, 0)
goal = (5, 5)
solution = solve_maze(maze, start, goal)
if solution:
print("Path to goal:")
for step in solution:
print(step)
else:
print("No path found.")
One small but important detail: this code’s is_valid only allows stepping onto '.', so S and G are treated as labels in the diagram, not actual characters in the grid. In practice, you either keep the grid as dots and track start/goal separately (like this code does), or you expand is_valid to allow both 'S' and 'G' and restore each cell's original character after exploring it. The version here is consistent because the grid itself is all '.' and '#', and the goal is a coordinate.
explore(x, y)I. Base Cases:
(x, y) is not valid (out of bounds, wall, or visited), return False.(x, y) equals the goal position, append it to path and return True.II. Recursive Exploration:
(x, y) as visited by setting maze[x][y] = 'V'.(x, y) to the path.explore(x + 1, y)explore(x - 1, y)explore(x, y + 1)explore(x, y - 1)'.' and propagate True, retaining the successful path. The input grid is restored on both successful and failed searches.III. Backtracking:
(x, y) from path using path.pop().maze[x][y] = '.'.False to indicate that this path does not lead to the goal.This is the heart of backtracking in one function: mark, explore, undo. The “undo” is what keeps the search honest. Here visited marks belong to the current path and are undone so alternative simple paths can reuse cells. For finding any route in a static maze, a permanent visited set is also correct and avoids repeated exploration. Path-dependent puzzles, such as word search, need more careful state tracking.
I. Start at (0, 0):
(0, 0) as visited and adds it to the path.II. Explore Neighbors:
(1, 0).III. Recursive Exploration:
(1, 0), continues moving Down to (2, 0).(2, 0), first explores downward through (3, 0) into the dead end along row 4. After undoing those moves, it tries right to (2, 1).IV. Dead Ends and Backtracking:
V. Reaching the Goal:
(5, 5) if a path exists.True, and the full path is constructed via the recursive calls.The path from start to goal:
[(0, 0), (1, 0), (2, 0), (2, 1), (2, 2), (1, 2),
(1, 3), (0, 3), (0, 4), (1, 4), (1, 5), (2, 5),
(3, 5), (4, 5), (5, 5)]
Let's overlay the path onto the maze for better visualization. We'll use * to indicate the path.
Maze with Path:
0 1 2 3 4 5
+---+---+---+---+---+---+
0 | * | . | # | * | * | . |
+---+---+---+---+---+---+
1 | * | # | * | * | * | * |
+---+---+---+---+---+---+
2 | * | * | * | . | # | * |
+---+---+---+---+---+---+
3 | . | # | # | # | . | * |
+---+---+---+---+---+---+
4 | . | . | . | # | . | * |
+---+---+---+---+---+---+
5 | # | # | # | # | . | * |
+---+---+---+---+---+---+
Legend:
* - Path taken
# - Wall
. - Open path
For a rectangular grid with $R$ rows and $C$ columns, this path-based search uses $O(RC)$ auxiliary space but can explore exponentially many simple paths. A DFS with permanent visited marks finds any reachable goal in $O(RC)$ time; BFS has the same time bound and finds a shortest path when every move has equal cost.
explore function to include additional directions.Choose the search method according to the required result:
The reusable pattern is to represent partial choices, check constraints, explore a candidate, and restore state. Understanding these responsibilities makes it easier to adapt the method to other constraint problems.
Develop an algorithm to generate all possible permutations of a given list of elements. This problem requires creating different arrangements of the elements where the order matters.
Design an algorithm to generate all possible combinations of 'k' elements selected from a given list of elements. This involves selecting elements where the order does not matter, but the selection size does.
Determine whether pattern symbols can be mapped to nonempty substrings so that concatenating their mappings reproduces the entire input string. Repeated occurrences of a symbol must use the same substring. For example, abab can match redblueredblue with a → red and b → blue. State separately whether distinct symbols must have distinct mappings; that is an additional constraint, not ordinary wildcard matching.
Given a board of letters and a dictionary, find dictionary words formed by paths through neighboring cells. This exercise allows horizontal, vertical, and diagonal moves. Track cells used in the current path, define whether reuse is allowed, and reject prefixes that cannot lead to dictionary words.
Create an algorithm that identifies whether a simple path exists within a provided undirected or directed graph. This path should visit every vertex exactly once. A Hamiltonian path visits every vertex once; a Hamiltonian cycle also returns to its start. The traveling salesperson problem is a related optimization problem that asks for a minimum-weight tour. Backtracking can enumerate candidate paths and reject repeated vertices.
Develop an algorithm to find all possible ways to color a given graph with 'k' colors such that no two adjacent vertices share the same color. This graph coloring problem requires ensuring valid color assignments for all vertices.
Create an algorithm to find all potential paths a knight can take on an 'n' x 'n' chessboard to visit every square exactly once. This classic chess problem involves ensuring the knight moves in an L-shape and covers all board positions.
Enumerate topological orderings of a directed acyclic graph by repeatedly choosing an available zero-indegree vertex, updating indegrees, and undoing the choice. If only one ordering is needed, a standard topological sort is more efficient. This involves sorting the vertices such that for every directed edge UV from vertex U to vertex V, U comes before V in the ordering.
Develop an algorithm to determine the optimal move for a player in a game of tic-tac-toe using the minimax algorithm. This requires evaluating possible moves to find the best strategy for winning or drawing the game.