DEV Community

Timevolt
Timevolt

Posted on

Backtracking: The Inception of Problem Solving

The Quest Begins (The "Why")

I still remember the first time I stared at a partially filled Sudoku grid in an interview prep book, feeling like I was trying to solve a Rubik’s Cube blindfolded. The brute‑force idea—try every number in every empty cell until something fits—seemed obvious, but it exploded into millions of possibilities and froze my laptop. I was stuck in a loop, watching the clock tick down, and I thought, “There has to be a smarter way to search.”

That moment kicked off a quest: find a technique that builds a solution step by step, backs out when it hits a dead end, and still guarantees we’ll eventually hit the right answer—without enumerating every impossible combination. Enter backtracking, the algorithm that feels less like a random guess and more like a guided tour through a maze, where you leave breadcrumbs so you never retrace the same wrong path.

The Revelation (The Insight)

So why does backtracking work?

Imagine you’re constructing a solution piece by piece—placing a number in a Sudoku cell, or putting a queen on a chessboard row. After each placement you ask: Does this partial board still have a chance to become a full solution? If the answer is yes, you keep going deeper. If it’s no, you undo the last move (the “backtrack”) and try the next alternative.

The magic is that you never discard a promising prefix prematurely. By checking constraints locally (row/column/box for Sudoku, column/diagonals for N‑Queens) you prune huge swaths of the search tree as soon as they become impossible. You’re not exploring every leaf; you’re only walking down paths that could still succeed.

Because each step does only a constant‑amount of work (checking a few constraints), the algorithm’s efficiency hinges on how much pruning you achieve. In the worst case you still might visit an exponential number of nodes, but for Sudoku and N‑Queens the constraints are strong enough that the effective branching factor drops dramatically—making the solution feel instantaneous for typical puzzle sizes.

Wielding the Power (Code & Examples)

Sudoku Solver – the classic interview favorite

def solve_sudoku(board):
    """Modify board in‑place; returns True if solved."""
    empty = find_empty(board)
    if not empty:                     # no empty cells → solved
        return True
    row, col = empty

    for num in map(str, range(1, 10)):   # try '1' .. '9'
        if is_valid(board, row, col, num):
            board[row][col] = num       # make a choice
            if solve_sudoku(board):     # recurse
                return True
            board[row][col] = '.'       # ← backtrack: undo choice
    return False                         # trigger backtracking upstream

def find_empty(board):
    for r in range(9):
        for c in range(9):
            if board[r][c] == '.':
                return r, c
    return None

def is_valid(board, row, col, num):
    # check row
    if any(board[row][c] == num for c in range(9)):
        return False
    # check column
    if any(board[r][col] == num for r in range(9)):
        return False
    # check 3×3 box
    sr, sc = 3 * (row // 3), 3 * (col // 3)
    for r in range(sr, sr + 3):
        for c in range(sc, sc + 3):
            if board[r][c] == num:
                return False
    return True
Enter fullscreen mode Exit fullscreen mode

Why this feels like a victory:

The moment I saw the board fill itself after a few recursive calls, I felt like I’d just defeated the final boss in a RPG—every wrong guess was instantly undone, and the algorithm kept marching forward until the solution emerged.

Common trap: Forgetting to reset board[row][col] = '.' after the recursive call. If you leave the tentative value there, later branches see a corrupted board and either miss solutions or produce invalid ones. The backtrack step is non‑negotiable.

N‑Queens – place queens so none attack each other

def solve_n_queens(n):
    """Return a list of all distinct solutions."""
    solutions = []
    cols = set()          # occupied columns
    diag1 = set()         # r - c
    diag2 = set()         # r + c

    def backtrack(r, state):
        if r == n:                     # all rows filled → solution found
            solutions.append(state.copy())
            return
        for c in range(n):
            if c in cols or (r - c) in diag1 or (r + c) in diag2:
                continue                # this column/diagonal is blocked
            # place queen
            cols.add(c)
            diag1.add(r - c)
            diag2.add(r + c)
            state.append('.' * c + 'Q' + '.' * (n - c - 1))

            backtrack(r + 1, state)    # recurse to next row

            # ← backtrack: remove queen and clean sets
            state.pop()
            cols.remove(c)
            diag1.remove(r - c)
            diag2.remove(r + c)

    backtrack(0, [])
    return solutions
Enter fullscreen mode Exit fullscreen mode

Why this shines:

By storing occupied columns and diagonals in sets, each safety check is O(1) instead of scanning the whole board. The recursion depth is at most n, so the algorithm feels linear in the amount of work per node—pure backtracking power.

Typical slip‑up: Using a list to track columns and then doing if c in cols: which is O(n) per check. For n = 14 the difference is negligible, but for larger n it turns the algorithm from snappy to sluggish. The set trick is a small optimization that pays off big time.

Why This New Power Matters

Backtracking isn’t just a party trick for puzzles; it’s a general‑purpose strategy for any problem where you can construct a solution incrementally and validate partial candidates. Think of it as your go‑to tool for:

  • Constraint satisfaction (graph coloring, scheduling)
  • Combinatorial generation (subsets, permutations, Hamiltonian paths)
  • Even some AI search problems where you need to explore a game tree with pruning

Once you internalize the “try, validate, undo” loop, you start seeing opportunities to apply it everywhere—turning what once felt like exhaustive search into an elegant, guided exploration.

Your Next Quest

Grab a Sudoku puzzle from your newspaper (or generate one online) and try to implement the solver above in your favorite language. Then, take the N‑Queens code and tweak it to count solutions instead of storing them—watch how the same backtracking core adapts with barely any change.

If you’re feeling ambitious, modify the Sudoku solver to detect unsolvable boards early, or add a heuristic that picks the cell with the fewest possibilities first (minimum remaining values).

What will you build with this newfound power? Drop a link to your gist or a snippet in the comments—I’d love to see how you wield backtracking in your own adventures!


Happy coding, and may your recursion always find a base case!

Top comments (0)