Sudoku Solving

null

Overview

I recently was trying to solve the Sudoku solver problem. I’ll be honest: I didn’t finish in time. However, I’m not a fan of leaving stones unturned. I spent the evening diving back into the logic to understand not just how to solve it, but why certain patterns work.

This post isn’t a "look how I crushed it" narrative. It’s a breakdown of where I stalled, the mathematical "aha!" moment that cleared the fog, and how I refined the final implementation to be more efficient. If you’ve ever found recursion or backtracking a bit daunting, this process might feel familiar.

The Problem

Given a partially filled 9×9 Sudoku board, fill in the missing cells such that:

  • Each row contains digits 1–9 exactly once

  • Each column contains digits 1–9 exactly once

  • Each 3×3 sub-grid contains digits 1–9 exactly once

My Initial Thought Process

I quickly realized that validating rows and columns was straightforward—just a simple iteration. However, the 3×3 sub-grids presented a mental block: how do you efficiently map any given (row, col) coordinate to its corresponding 3×3 block?

I knew the logic involved dividing by 3 to "group" the indices, but translating that back into a starting coordinate for a loop felt counter-intuitive in the heat of the moment. I didn't want to just memorize a formula; I wanted to understand the underlying geometry of the grid.

The Logic for Identifying a Subblock

By dividing by 3, you identify which subblock does the cell belong to

For example:

  • Rows 0,1,2 → group 0

  • Rows 3,4,5 → group 1

  • Rows 6,7,8 → group 2

Same idea applies to columns.

Once you know the group, multiplying by 3 gives you the starting index of that 3×3 box.

The formulas look like this:

boxRow=⌊row/3⌋×3

boxCol=⌊col/3⌋×3

Coding it out, they look like this:

const boxRow = Math.floor(row / 3) * 3;
const boxCol = Math.floor(col / 3) * 3;

So if:

  • row = 5 → Math.floor(5 / 3) = 1 → 1 * 3 = 3

  • col = 7 → Math.floor(7 / 3) = 2 → 2 * 3 = 6

That means the cell belongs to the box that starts at:

(3, 6)

Checking for Candidates

Now moving forward, the next step is to actually use those constraints.

The approach I took was:

  • Iterate through the board from left to right, top to bottom

  • For each empty cell .), determine what values are valid  

At a high level:

For each cell:

 If it's empty:
    Gather all numbers already used in:
      - its row
      - its column
      - its 3x3 subgrid

Return the numbers that have not been seen as they are valid candidates

At this point, I felt like I had something solid. I could look at any empty cell and say:

Here are all the possible values that won’t break the rules.

The next challenge though was picking among all the possible candidates. If we think about it from a greedy perspective, the natural instinct is to:

Just pick a candidate and move forward.

Maybe even:

Pick one randomly and hope it works.

However, then the obvious question comes up:

What happens if that choice leads to a dead end? How would we recover?

This is where the core idea finally clicked for me. Make a choice, and then make a recursive call on the board. For every recursive call, we revisit the board — all the cells, their rows, columns, and 3×3 subgrids — checking which numbers are still valid. Progress until

  1. We fill the entire board successfully

    • Every empty cell has a valid number  

    • No conflicts remain  

    • We can return the completed board

  2. We hit a dead end

    • An empty cell has no valid candidates  

    • That means the previous choice was wrong  

    • We undo that choice (backtrack) and try the next candidate

Why It Works

The core of the solver is Recursive Backtracking.

The natural instinct is to be "greedy"—pick a valid number and move on. But Sudoku is deterministic; one wrong choice early on can make the board impossible to solve 40 steps later. Backtracking solves this by:

  1. Making a choice.

  2. Moving to the next cell.

  3. If we hit a "dead end" (a cell with zero valid candidates), we undo the last choice and try the next available candidate.

This transforms the problem from "guessing" to a systematic exploration of a decision tree, ensuring we find the solution if it exists.

Code

// Helper function: get all valid candidates for a cell
function getCandidates(board, row, col) {
  const usedNums = new Set();

  // Check row and column
  for (let i = 0; i < 9; i++) {
    if (board[row][i] !== ".") usedNums.add(board[row][i]);
    if (board[i][col] !== ".") usedNums.add(board[i][col]);
  }

  // Check 3x3 subgrid
  const boxRow = Math.floor(row / 3) * 3;
  const boxCol = Math.floor(col / 3) * 3;
  for (let r = boxRow; r < boxRow + 3; r++) {
    for (let c = boxCol; c < boxCol + 3; c++) {
      if (board[r][c] !== ".") usedNums.add(board[r][c]);
    }
  }

  // Collect candidates
  const candidates = [];
  for (let num = 1; num <= 9; num++) {
    if (!usedNums.has(num.toString())) {
      candidates.push(num.toString());
    }
  }

  return candidates;
}

// Recursive backtracking solver
function solve(board) {
  for (let row = 0; row < 9; row++) {
    for (let col = 0; col < 9; col++) {
      if (board[row][col] === ".") {
        const candidates = getCandidates(board, row, col);
        for (const cand of candidates) {
          board[row][col] = cand; // try a candidate
          if (solve(board)) return true; // success!
          board[row][col] = "."; // undo if it fails
        }
        return false; // no candidates worked → backtrack
      }
    }
  }
  return true; // all cells filled
}

// Main function
function sudokuSolve(board) {
  solve(board);
  return board;
}

// Example usage
const board = [
  [".", ".", ".", "7", ".", ".", "3", ".", "1"],
  ["3", ".", ".", "9", ".", ".", ".", ".", "."],
  [".", "4", ".", "3", "1", ".", "2", ".", "."],
  [".", "6", ".", "4", ".", ".", "5", ".", "."],
  [".", ".", ".", ".", ".", ".", ".", ".", "."],
  [".", ".", "1", ".", ".", "8", ".", "4", "."],
  [".", ".", "6", ".", "2", "1", ".", "5", "."],
  [".", ".", ".", ".", ".", "9", ".", ".", "8"],
  ["8", ".", "5", ".", ".", "4", ".", ".", "."]
];

console.log(sudokuSolve(board));

Conclusion

Stepping back, this exercise reminded me that technical interviews aren't just about reaching a "solved" state within 40 minutes—they are about how you navigate the unknown. While I didn't finish the solver during the session, deconstructing the sub-grid indexing and recursive state afterward turned a stressful moment into a permanent part of my technical toolkit.

In a real-world application, I’d likely optimize this further—perhaps by passing coordinates to the recursive calls to avoid re-scanning the board, or using bitmasking for faster candidate lookups. However, there is immense value in mastering the readable, deterministic logic first.