DEV Community

Timevolt
Timevolt

Posted on

Backtracking: A Sudoku Quest Inspired by Inception

The Quest Begins (The “Why”)

I remember staring at a half‑filled Sudoku grid during a late‑night interview prep session, feeling like I was trapped in a dream within a dream. Every time I placed a number, I’d hit a wall a few steps later and have to erase everything—frustrating, right? The interviewer kept nudging me: “Can you solve it without brute‑forcing every possibility?” That question was the dragon I needed to slay. I wanted a method that knew when to back up, when to try another path, and when to declare victory—without exploding the search space.

That’s when backtracking clicked for me. It isn’t just a fancy term for “try‑and‑error”; it’s a principled way to explore a decision tree while pruning branches that can’t possibly lead to a solution. Understanding why it works turned a tedious guessing game into a confident, almost magical, solve‑it‑in‑one‑pass algorithm.

The Revelation (The Insight)

The Core Idea

Imagine you’re assembling a LEGO set. You snap a brick onto the model, step back, and ask: “Does this placement still allow me to finish the set?” If the answer is yes, you keep going. If it’s no, you yank that brick off, try a different spot, and repeat.

Backtracking does exactly that for problems like Sudoku or N‑Queens:

  1. Make a choice (place a number, put a queen).
  2. Check constraints (no duplicate in row/col/box; no two queens attack each other).
  3. If constraints hold, recurse to fill the next empty cell.
  4. If we hit a dead end, undo the last choice (backtrack) and try the next alternative.

The magic is in step 3: we only go deeper when the partial solution is still viable. This pruning eliminates huge swaths of the search space before we even explore them. In the worst case we might still visit every node, but for typical puzzles the pruning is dramatic—often reducing the search from exponential to something that feels linear in practice.

Why It Guarantees a Solution (If One Exists)

Because we systematically try every legal option at each depth, we never skip a potential solution. If a solution exists, the recursion will eventually follow the exact sequence of choices that builds it, and when we reach a full board we return true. If we exhaust all options at a level and backtrack all the way to the start, we know no arrangement satisfies the constraints.

In short: backtracking = depth‑first search + constraint checking + undo. It’s simple, but the why—the guarantee that we never miss a valid configuration while discarding impossible ones early—is what makes it powerful.

Wielding the Power (Code & Examples)

The Struggle: Naïve Brute Force

# Sudoku – try every number 1‑9 in every empty cell (no pruning)
def solve_brute(board):
    empty = find_empty(board)
    if not empty:
        return True                     # solved
    r, c = empty
    for num in range(1, 10):
        board[r][c] = num
        if solve_brute(board):          # naïve recursion, no check
            return True
        board[r][c] = 0                 # undo
    return False
Enter fullscreen mode Exit fullscreen mode

This version will still work, but it wastes time checking boards that already break Sudoku rules after just a few placements. For a tough puzzle it can take seconds or even minutes.

The Victory: Backtracking with Constraint Checks

def solve_sudoku(board):
    empty = find_empty(board)
    if not empty:               # no empty cells → solved
        return True
    r, c = empty

    for num in range(1, 10):
        if is_valid(board, r, c, num):
            board[r][c] = num                     # make choice
            if solve_sudoku(board):                # recurse
                return True
            board[r][c] = 0                         # undo (backtrack)
    return False                                   # trigger backtrack

def is_valid(board, r, c, num):
    # row & column
    for i in range(9):
        if board[r][i] == num or board[i][c] == num:
            return False
    # 3×3 box
    box_r, box_c = 3 * (r // 3), 3 * (c // 3)
    for i in range(box_r, box_r + 3):
        for j in range(box_c, box_c + 3):
            if board[i][j] == num:
                return False
    return True

def find_empty(board):
    for i in range(9):
        for j in range(9):
            if board[i][j] == 0:
                return i, j
    return None
Enter fullscreen mode Exit fullscreen mode

What changed?

  • Before recursing we call is_valid. If the placement violates any Sudoku rule, we skip the whole subtree.
  • The undo step (board[r][c] = 0) is explicit—this is the backtrack.

N‑Queens – Another Classic

def solve_n_queens(n):
    board = [-1] * n          # board[row] = col where queen sits
    def backtrack(row):
        if row == n:          # all queens placed
            return True
        for col in range(n):
            if is_safe(board, row, col):
                board[row] = col
                if backtrack(row + 1):
                    return True
                board[row] = -1   # undo
        return False
    return backtrack(0)

def is_safe(board, row, col):
    for r in range(row):
        c = board[r]
        if c == col or abs(c - col) == row - r:
            return False
    return True
Enter fullscreen mode Exit fullscreen mode

Same pattern: try a column, verify safety (no same column, no diagonal), recurse, undo on failure.

Common Traps to Avoid

Trap Why it hurts Fix
Forgetting to undo the choice Leaves the board polluted, causing false negatives/positives Always reset after the recursive call (board[r][c] = 0 or board[row] = -1)
Checking validity after recursion You’ve already wasted time exploring an impossible subtree Validate before recursing
Using global mutable state without clean‑up Hard to reason about, leads to bugs in concurrent settings Pass state explicitly or restore it in the undo step

Why This New Power Matters

Armed with a solid backtracking template, you can tackle a raft of interview favorites: Sudoku, N‑Queens, word search, graph coloring, even generating all permutations or subsets. The technique transforms an apparently exponential nightmare into a manageable depth‑first walk where impossible branches are chopped off early.

More than just a coding trick, backtracking teaches you to think in terms of constraints and state restoration—a mindset that shows up in everything from parsing languages to AI planning. When you can explain not just how the algorithm works but why it prunes the search space, you stand out in interviews and feel confident tackling any combinatorial puzzle that comes your way.

Your Next Quest

Pick a constraint‑satisfaction problem you’ve seen on LeetCode or HackerRank (maybe “Letter Tile Possibilities” or “Sudoku Solver”). Write the backtracking solution from scratch, focusing on the validation step and the undo. Then, try to add a simple heuristic—like choosing the cell with the fewest possibilities first—to see how the runtime improves.

Feel free to drop your code or a link in the comments; I’d love to see how you’ve leveled up your backtracking game!


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

Top comments (0)