
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 candidatesAt 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
We fill the entire board successfully
Every empty cell has a valid number
No conflicts remain
We can return the completed board
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:
Making a choice.
Moving to the next cell.
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.