DEV Community

Timevolt
Timevolt

Posted on

Backtracking: The Back to the Future of Algorithms

The Quest Begins (The "Why")

I still remember my first coding interview like it was yesterday. The interviewer slid a Sudoku board across the table and said, “Make this solve itself.” My brain went blank. I could picture the 81 cells, but the sheer number of possibilities felt like trying to count every grain of sand on a beach. I started writing a nested loop that tried every digit in every empty cell—pure brute force. After a few minutes of watching the program churn through billions of combos, I realized I was stuck in a time loop, reminiscent of Marty McFly’s DeLorean stuck in 1955. I needed a way to jump forward without exploring every dead‑end path. That’s when backtracking showed up like a Doc Brown‑style gadget: it lets you try a choice, see if it leads to a solution, and if not, rewind instantly and try another path.

The Revelation (The Insight)

Backtracking isn’t just “try everything until it works.” It’s a guided depth‑first search that prunes impossible branches the moment they violate a constraint. Think of the solution space as a tree: each level corresponds to a decision (which number to place in a cell, which row to put a queen, etc.). As soon as you place a piece, you check the rules. If the placement breaks Sudoku’s row/column/box rule or puts two queens on the same diagonal, you backtrack—you undo that move and try the next alternative.

Why does this work so well? Because most of the tree is useless. In Sudoku, after you fill a few cells, the constraints eliminate dozens of possibilities for the remaining spots. Instead of exploring 9^81 leaf nodes (astronomically huge), you only walk down the paths that survive the checks. The algorithm’s power comes from early failure detection: the earlier you spot a conflict, the larger the subtree you discard. It’s like Marty noticing a paradox before he even steps out of the DeLorean—he can avoid the whole messed‑up timeline.

Wielding the Power (Code & Examples)

The Naïve Attempt (the trap)

def solve_sudoku_bruteforce(board):
    empties = [(r, c) for r in range(9) for c in range(9) if board[r][c] == 0]
    for combo in product(range(1, 10), repeat=len(empties)):
        # fill board with combo
        for (r, c), val in zip(empties, combo):
            board[r][c] = val
        if is_valid(board):
            return True
        # reset board (expensive!)
        for r, c in empties:
            board[r][c] = 0
    return False
Enter fullscreen mode Exit fullscreen mode

Why it’s a trap:

  • product generates 9^n combos where n is the number of empty cells.
  • Even after finding a conflict deep in the combo, we still waste time constructing the whole board before checking.
  • Resetting the board after each failed attempt is O(n²) extra work.

The Backtracking Victory

def solve_sudoku(board):
    def backtrack():
        # find next empty cell
        for r in range(9):
            for c in range(9):
                if board[r][c] == 0:
                    # try each digit
                    for d in range(1, 10):
                        if safe(board, r, c, d):
                            board[r][c] = d          # make choice
                            if backtrack():          # recurse
                                return True
                            board[r][c] = 0          # undo choice (backtrack)
                    return False                     # trigger backtracking
        return True                                 # no empty cells → solved

    def safe(b, r, c, d):
        # row & column
        if any(b[r][j] == d for j in range(9)) or any(b[i][c] == d for i in range(9)):
            return False
        # 3×3 box
        br, bc = 3 * (r // 3), 3 * (c // 3)
        return all(b[i][j] != d for i in range(br, br+3) for j in range(bc, bc+3))

    return backtrack()
Enter fullscreen mode Exit fullscreen mode

What changed?

  1. Early pruning: safe checks the moment we place a digit. If it fails, we never go deeper.
  2. Immediate undo: Setting the cell back to 0 is O(1); we don’t rebuild the whole board.
  3. Depth‑first recursion: The call stack holds the current path, so backtracking is just returning from a frame.

Another Interview Favorite: N‑Queens

def solve_n_queens(n):
    def backtrack(row, cols, diag1, diag2):
        if row == n:
            return True
        for col in range(n):
            d1 = row - col          # main diagonal
            d2 = row + col          # anti‑diagonal
            if col in cols or d1 in diag1 or d2 in diag2:
                continue            # conflict → prune
            # place queen
            cols.add(col); diag1.add(d1); diag2.add(d2)
            if backtrack(row + 1, cols, diag1, diag2):
                return True
            # remove queen (backtrack)
            cols.remove(col); diag1.remove(d1); diag2.remove(d2)
        return False

    return backtrack(0, set(), set(), set())
Enter fullscreen mode Exit fullscreen mode

Why this shines:

  • The three sets (cols, diag1, diag2) let us test conflicts in O(1) time.
  • As soon as a queen creates a clash, we skip the entire subtree of deeper rows.
  • The algorithm explores only promising configurations, turning an O(n!) nightmare into something that solves n=15 in a blink.

Common Pitfalls (the “traps”)

Mistake What happens Fix
Forgetting to undo a change (board[r][c] = d without resetting) The board stays polluted; later branches inherit wrong values → false negatives/positives Always revert after the recursive call (board[r][c] = 0).
Checking validity only after filling the whole board You waste time on obviously dead ends Validate incrementally as shown with safe.
Using mutable default arguments (e.g., def backtrack(row, cols=[])) The same list is shared across calls, causing bizarre state leaks Pass fresh copies or use explicit parameters as above.

Why This New Power Matters

Armed with backtracking, you can tackle a whole class of interview puzzles: Sudoku, N‑Queens, Word Search, Graph Coloring, even configuring a party seating plan where no two enemies sit together. The technique transforms an exponential brute‑force nightmare into a manageable search by leveraging problem‑specific constraints to cut off useless work early.

In real‑world terms, think of backtracking as your GPS that constantly recalculates when you hit a roadblock—except instead of waiting for a satellite signal, you know the blockage the moment you violate a rule, and you instantly reroute.

The next time you see a puzzle that feels like “try every combination,” ask yourself: What constraint can I check right now? If you can answer that, you’ve just unlocked a backtracking solution.

Your Turn

Grab a Sudoku from the newspaper (or generate one online) and time how long the naïve brute force takes versus the backtracking version. Then try scaling N‑Queens to n=12 or n=13 and watch the solution appear almost instantly.

Challenge: Modify the N‑Queens code to print all distinct solutions instead of just one. How does the runtime change? Share your results or any tweaks you discover—let’s keep the quest going!

Happy backtracking! 🚀

Top comments (0)