The Quest Begins (The "Why")
I still remember the first time I stared at a Sudoku puzzle in a coffee shop, pencil poised, and felt my brain short‑circuit. I could fill in the obvious numbers, but as soon as I hit a dead end I’d erase everything and start over—again and again. It was maddening, and I kept thinking there had to be a smarter way than brute‑force guessing.
Around the same time, a friend threw the classic N‑Queens problem at me during an interview prep session: “Place n queens on an n×n board so none attack each other.” My first attempt was a nested‑loop nightmare that checked every permutation—O(n!)—and it choked on n = 8. I felt like I was trying to defeat a boss with a wooden sword.
That frustration sparked a question: What if we could explore possibilities intelligently, backing up the moment we realize we’re on a dead‑end path? That’s exactly what backtracking gives us—a systematic yet flexible way to walk through a search tree, pruning branches that can’t possibly lead to a solution.
The Revelation (The Insight)
At its heart, backtracking is depth‑first search with a guard rail. Imagine you’re navigating a maze. You pick a direction, walk forward, and if you hit a wall you immediately turn around and try the next path. You never waste time wandering down a corridor that you already know leads nowhere.
In algorithmic terms:
- Choose – make a decision (place a number, put a queen).
- Constrain – check if the partial solution still satisfies all rules.
- Recurse – if it’s valid, go deeper; otherwise, undo the last choice and try the next alternative.
The “undo” step is the magic. By reverting state instead of starting from scratch, we reuse work already done. The search space is still exponential in the worst case, but pruning cuts off huge swaths of it early, making puzzles that would take hours solvable in milliseconds.
Why does this work? Because any solution must be a leaf in the decision tree where every constraint holds. If a node violates a constraint, none of its descendants can be valid—so we can safely prune the entire subtree. That logical guarantee is why backtracking is both correct and, when constraints are tight, surprisingly fast.
Wielding the Power (Code & Examples)
Let’s see the pattern in code. I’ll write a generic backtracking skeleton first, then specialize it for Sudoku and N‑Queens.
def backtrack(state, is_valid, choices, goal_test):
"""
state – current partial solution (list)
is_valid – fn(state) -> bool, checks constraints so far
choices – fn(state) -> iterable of next candidates
goal_test – fn(state) -> bool, true when we have a complete solution
"""
if goal_test(state):
return state[:] # found a solution
for cand in choices(state):
state.append(cand) # choose
if is_valid(state):
result = backtrack(state, is_valid, choices, goal_test)
if result: # propagate success upward
return result
state.pop() # undo (backtrack)
return None # dead end
Sudoku Solver
A Sudoku board is a 9×9 grid where each row, column, and 3×3 box must contain digits 1‑9 exactly once.
def solve_sudoku(board):
EMPTY = 0
def is_valid(b):
# check rows, cols, boxes for duplicates (ignore zeros)
for i in range(9):
row = [b[i][j] for j in range(9) if b[i][j] != EMPTY]
if len(row) != len(set(row)):
return False
col = [b[j][i] for j in range(9) if b[j][i] != EMPTY]
if len(col) != len(set(col)):
return False
for bi in range(0, 9, 3):
for bj in range(0, 9, 3):
block = [
b[i][j]
for i in range(bi, bi+3)
for j in range(bj, bj+3)
if b[i][j] != EMPTY
]
if len(block) != len(set(block)):
return False
return True
def next_empty(b):
for i in range(9):
for j in range(9):
if b[i][j] == EMPTY:
return i, j
return None
def choices(b):
pos = next_empty(b)
if not pos:
return [] # board full
i, j = pos
used = set()
used.update(b[i]) # row
used.update(b[x][j] for x in range(9)) # column
bi, bj = (i//3)*3, (j//3)*3
used.update(b[x][y] for x in range(bi, bi+3) for y in range(bj, bj+3))
return [d for d in range(1,10) if d not in used]
def goal(b):
return next_empty(b) is None
# start recursion
backtrack(board, is_valid, choices, goal)
return board # solved in‑place
Why this works:
-
is_validguarantees we never continue down a path that already breaks a rule. -
choicesonly proposes numbers that aren’t already present in the row/col/box, drastically shrinking the branching factor. - The recursion depth is at most 81 (the number of cells), so space is O(n²) for the board plus O(n²) call stack—effectively O(n²).
N‑Queens
Place n queens on an n×n board so no two share a row, column, or diagonal.
def solve_n_queens(n):
def is_valid(board):
# board[col] = row where queen is placed; -1 means未放置
for c1 in range(n):
r1 = board[c1]
if r1 == -1: continue
for c2 in range(c1+1, n):
r2 = board[c2]
if r2 == -1: continue
if r1 == r2 or abs(r1-r2) == abs(c1-c2):
return False
return True
def choices(board):
# find first column without a queen
try:
col = board.index(-1)
except ValueError:
return [] # all placed
# try every row in this column
return [row for row in range(n) if row not in board]
def goal(board):
return -1 not in board
board = [-1] * n
backtrack(board, is_valid, choices, goal)
return board # list of rows per column
Why this works:
- The
choicesfunction guarantees we never put two queens in the same column (by construction) and we skip rows already used. - The diagonal check in
is_validkills any branch where a new queen would attack an existing one. - Depth is n, so auxiliary space is O(n).
- In the worst case we still explore O(n!) leaves, but strong pruning reduces the explored nodes dramatically—n = 14 solves in a fraction of a second on a laptop.
Why This New Power Matters
Armed with backtracking, you can tackle a whole class of constraint‑satisfaction puzzles that appear in interviews: word searches, cross‑word filling, graph coloring, even scheduling problems. The pattern is universal:
- Encode the problem as a sequence of decisions.
- Define a fast “partial‑solution‑check”.
- Let the recursion do the heavy lifting, undoing when needed.
Because the algorithm is conceptual rather than tied to a specific data structure, you can adapt it to languages ranging from Python to C++ to JavaScript in minutes. And the best part? You’ll walk out of an interview feeling like you’ve just defeated the final boss with a lightsaber—elegant, precise, and unstoppable.
Your Turn
Pick a constraint problem you’ve struggled with (maybe a Kakuro puzzle or a Hamiltonian path). Write the is_valid, choices, and goal_test functions, plug them into the skeleton above, and watch the solution emerge. Share your code or a snippet in the comments—I’d love to see what you conquer next! 🚀
Top comments (0)