LeetCode #37 — Hard

Sudoku Solver

A complete visual walkthrough of the backtracking algorithm — filling empty cells one by one, undoing choices when stuck.

Solve on LeetCode
The Problem

Problem Statement

Write a program to solve a Sudoku puzzle by filling the empty cells.

A sudoku solution must satisfy all of the following rules:

  1. Each of the digits 1-9 must occur exactly once in each row.
  2. Each of the digits 1-9 must occur exactly once in each column.
  3. Each of the digits 1-9 must occur exactly once in each of the 9 3x3 sub-boxes of the grid.

The '.' character indicates empty cells.

Example 1:

Input: board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
Output: [["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
Explanation: The input board is shown above and the only valid solution is shown below:


Constraints:

  • board.length == 9
  • board[i].length == 9
  • board[i][j] is a digit or '.'.
  • It is guaranteed that the input board has only one solution.
Patterns Used

Roadmap

  1. The Core Insight — Backtracking
  2. The Algorithm Step by Step
  3. Walkthrough — Watching Backtracking in Action
  4. The isValid Check
  5. Edge Cases
  6. Full Annotated Code
  7. Interactive Demo
  8. Complexity Analysis
Step 01

The Core Insight — Backtracking

Sudoku solving is a classic constraint satisfaction problem. The approach is elegant in its simplicity:

The Backtracking Recipe

1. Find an empty cell
Scan left-to-right, top-to-bottom for the first '.' cell.
2. Try digits 1-9
For each digit, check if placing it violates any Sudoku rule.
3. Place and recurse
If valid, place the digit and recursively try to solve the rest.
4. Backtrack if stuck
If no digit works, undo (set cell back to '.') and return false — the previous choice was wrong.
💡

Why does backtracking work? The problem guarantees exactly one solution. Backtracking systematically explores all possibilities, but the Sudoku constraints prune most branches immediately. In practice, it solves most puzzles in milliseconds.

Step 02

The Algorithm Step by Step

Here's the decision tree the algorithm builds. Each node is a choice for an empty cell:

Decision Flow

Cell (0,2): Try '1' — valid?
Yes! Place '1', recurse...
Cell (0,5): Try '1' — duplicate in row!
Cell (0,5): Try '2' — valid?
Yes! Place '2', recurse...
Cell (0,6): Try '1' — duplicate in row!
Cell (0,6): Try '2' — duplicate in row!
... no digit works ...
Backtrack! Remove '2' from (0,5)
Cell (0,5): Try '3' — valid?
Yes! Continue deeper...

The algorithm explores forward until stuck, then backtracks to try the next option. Think of it as navigating a maze — when you hit a dead end, you retrace your steps.

Step 03

Walkthrough — Watching Backtracking in Action

Consider a partially filled board. The algorithm finds the first empty cell and starts trying digits:

Before and After

INPUT (given cells)
5
3
.
.
7
.
.
.
.
6
.
.
1
9
5
.
.
.
.
9
8
.
.
.
.
6
.
8
.
.
.
6
.
.
.
3
4
.
.
8
.
3
.
.
1
7
.
.
.
.
2
.
.
.
6
.
6
.
.
.
.
2
8
.
.
.
.
4
1
9
.
.
5
.
.
.
.
8
.
.
7
9
OUTPUT (solved)
5
3
4
6
7
8
9
1
2
6
7
2
1
9
5
3
4
8
1
9
8
3
4
2
5
6
7
8
5
9
7
6
1
4
2
3
4
2
6
8
5
3
7
9
1
7
1
3
9
2
4
8
5
6
9
6
1
5
3
7
2
8
4
2
8
7
4
1
9
6
3
5
3
4
5
2
8
6
1
7
9
Step 04

The isValid Check

Before placing a digit, we must verify it doesn't violate any constraint. We check three things in a single loop:

Validity Check in One Loop

private boolean isValid(char[][] board, int row, int col, char c) {
    for (int i = 0; i < 9; i++) {
        if (board[row][i] == c) return false;         // check row
        if (board[i][col] == c) return false;         // check column
        if (board[3*(row/3)+i/3][3*(col/3)+i%3] == c)  // check 3x3 box
            return false;
    }
    return true;
}

The box index formula: 3*(row/3)+i/3 and 3*(col/3)+i%3 iterate through all 9 cells of the 3x3 box containing (row, col). As i goes 0..8, i/3 gives 0,0,0,1,1,1,2,2,2 for row offsets and i%3 gives 0,1,2,0,1,2,0,1,2 for column offsets.

Step 05

Edge Cases

Board with only one empty cell

Only one digit fits without conflicting with its row, column, and 3×3 box. The solver places it immediately and returns — no backtracking needed.

Deep backtracking required

Many placements look valid early on but lead to a dead end later. For example, placing 1 in cell (0,0) may force a conflict dozens of steps later, requiring the solver to unwind the entire chain.

Many empty cells (worst-case performance)

A nearly blank board forces the algorithm to explore a huge search tree. Constraint bitmasks (row/col/block arrays) prune invalid branches fast, keeping this tractable within the 9×9 bounds.

Board already complete

When no cells contain '.', the list of empty positions is empty. The DFS base case fires immediately (k == t.size()) and the board is returned unchanged.

Constraint propagation: only one valid digit

A cell where eight of nine digits are already used in its row, column, or box has exactly one candidate. The solver places it without branching, effectively performing unit propagation via the bitmask checks.

Step 06

Full Annotated Code

class Solution {
    public void solveSudoku(char[][] board) {
        solve(board);
    }

    private boolean solve(char[][] board) {
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.') continue;  // skip filled cells

                for (char c = '1'; c <= '9'; c++) {
                    if (isValid(board, i, j, c)) {
                        board[i][j] = c;           // place digit
                        if (solve(board))          // recurse
                            return true;            // solved!
                        board[i][j] = '.';         // backtrack
                    }
                }
                return false;  // no digit works → trigger backtracking
            }
        }
        return true;  // no empty cells left → solved!
    }

    private boolean isValid(char[][] board, int row, int col, char c) {
        for (int i = 0; i < 9; i++) {
            if (board[row][i] == c) return false;
            if (board[i][col] == c) return false;
            if (board[3*(row/3)+i/3][3*(col/3)+i%3] == c)
                return false;
        }
        return true;
    }
}
Step 07

Interactive Demo

Watch the solver fill in cells and backtrack when stuck. Each step shows one placement or one backtrack.

Backtracking Solver

Placements: 0
Backtracks: 0
Filled: 0/81
Step 08

Complexity Analysis

Time (worst)
O(9m)
Space
O(m)

Where m is the number of empty cells. Backtracking fills each empty cell by trying up to 9 digits. In the absolute worst case (an empty board), m = 81 and we explore up to 981 branches. Space is O(m) for the recursion stack -- one frame per empty cell.

Approach Breakdown

BRUTE FORCE
O(981) time
O(81) space

Try all 9 digits in every cell without checking constraints first. Each of the 81 cells branches 9 ways, giving 981 total paths. The recursion stack grows up to 81 frames deep for a fully empty board.

OPTIMAL
O(9m) time
O(m) space

Only m empty cells are explored, and the isValid check prunes invalid digits before recursing. This eliminates most branches immediately -- a typical puzzle with ~25 givens solves in under a millisecond. Stack depth is at most m.

🎯

Constraint propagation is the key. Checking row/col/box validity before placing a digit prunes most branches immediately. A typical puzzle with ~25 givens is solved in under a millisecond -- the effective branching factor is far less than 9 despite the exponential worst case.

Coach Notes

Common Mistakes

Review these before coding to avoid predictable interview regressions.

Missing undo step on backtrack

Wrong move: Mutable state leaks between branches.

Usually fails on: Later branches inherit selections from earlier branches.

Fix: Always revert state changes immediately after recursive call.