Sudoku puzzle completion challenge
You're sitting in an interview at Bloomberg, and the interviewer slides a partially filled Sudoku grid across the table. "Forget solving it by hand," they say. "Write me a program that solves any valid Sudoku board." This problem, also known as the "Sudoku Solver" on other platforms, is a classic test of your ability to implement constraint satisfaction through backtracking. It's a favorite at companies like Bloomberg, Microsoft, Adobe, and Google because it combines recursion, validation logic, and in-place mutation into one compact challenge.
TL;DR
Solve Sudoku by scanning left-to-right, top-to-bottom for the first empty cell, then trying digits '1' through '9'. For each candidate, check whether the digit already appears in the same row, column, or 3x3 sub-grid. If the placement is valid, recurse to fill the next empty cell. If no digit works, backtrack by resetting the cell to '.' and returning false. The recursion bottoms out when every cell is filled, returning true to signal success. Time complexity is O((9!)^9) in the worst case, though constraint pruning makes real puzzles solve in milliseconds.
Why This Problem Matters
Sudoku solving is the textbook example of backtracking with constraint propagation. In interviews, it tests whether you can decompose a complex problem into a clean recursive structure. The same pattern of "try, validate, recurse, backtrack" appears in problems like N-Queens, permutation generation, and word search. If you can implement a Sudoku solver cleanly, you've demonstrated mastery of a technique that transfers to dozens of other problems.
Understanding the Problem
Here's what we need to do:
Given: A 9x9 board where filled cells contain '1'-'9' and empty cells contain '.'
Task: Fill every '.' cell so that each row, column, and 3x3 sub-grid contains the digits 1-9 exactly once
Constraint: The input always has exactly one valid solution
Return: The completed board (modified in-place)
The first three rows of the example puzzle look like this. Blue cells are pre-filled, and purple cells are the empty slots we need to fill:
Loading visualization...
The Three Constraints
Every time we place a digit, it must satisfy all three Sudoku rules simultaneously:
- Row constraint: The digit does not appear elsewhere in the same row
- Column constraint: The digit does not appear elsewhere in the same column
- Sub-grid constraint: The digit does not appear in the same 3x3 box
For a cell like position (0,2), we need to check against the green cells (row), the purple cells (column), and the blue cells (3x3 sub-grid):
Loading visualization...
This decision tree shows what happens at cell (0,2). We try each digit, check constraints, and only proceed when a valid placement is found. If a later cell reaches a dead end, we unwind back here and try the next digit.
Edge Cases
- Nearly complete board: Only a few empty cells, so the solver finishes almost instantly
- Fully empty board: The worst case for backtracking, though still solvable because the first valid path through all 81 cells works
- Dense constraints: Boards where most cells are pre-filled tend to have fewer branching possibilities, making them faster to solve
Solution Approach
The algorithm follows a simple loop:
- Scan every cell from top-left to bottom-right
- When you find an empty cell, try placing digits
'1'through'9' - For each candidate digit, validate it against the row, column, and 3x3 sub-grid
- If the digit is valid, place it and recursively solve the rest of the board
- If the recursive call succeeds, propagate
trueupward - If it fails, reset the cell to
'.'(backtrack) and try the next digit - If no digit works, return
falseto trigger backtracking in the caller - When no empty cells remain, the puzzle is solved. Return
true.
The validation function isValid is the workhorse. It uses a single loop from 0 to 8 to check all three constraints simultaneously. For index i:
board[row][i]checks the entire rowboard[i][col]checks the entire columnboard[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3]checks the 3x3 sub-grid
That sub-grid formula is the tricky part. For a cell at (row, col), row / 3 * 3 gives the starting row of the sub-grid (using integer division). Adding i / 3 moves through the three rows of the sub-grid. Similarly, col / 3 * 3 + i % 3 walks through the three columns. As i goes from 0 to 8, you visit all nine cells of the sub-grid.
Implementation
Prefer a different language? Jump to solutions in other languages.
public class Solution {
public char[][] solveSudoku(char[][] board) {
solve(board);
return board;
}
private boolean solve(char[][] board) {
for (int row = 0; row < 9; row++) {
for (int col = 0; col < 9; col++) {
if (board[row][col] == '.') {
for (char num = '1'; num <= '9'; num++) {
if (isValid(board, row, col, num)) {
board[row][col] = num;
if (solve(board)) {
return true;
}
board[row][col] = '.';
}
}
return false;
}
}
}
return true;
}
private boolean isValid(char[][] board, int row, int col, char num) {
for (int i = 0; i < 9; i++) {
if (board[row][i] == num || board[i][col] == num
|| board[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3] == num) {
return false;
}
}
return true;
}
}
Let's trace through the key moments. The solve method scans cell by cell. It finds the first empty cell at (0,2). The isValid check rejects 1 (not present, wait, actually let's look: row 0 has 5, 3, 7, so 1 is not in the row. Column 2 has 8. The sub-grid has 5, 3, 6, 9, 8. So 1 is not a conflict, but downstream cells might force a backtrack). The solver places each valid candidate, recurses, and backtracks as needed.
The key line is board[row][col] = '.' after a failed recursive call. Without this reset, cells would retain incorrect values and pollute future constraint checks.
The Sub-Grid Index Formula
The expression board[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3] deserves a closer look. Consider cell (4, 7). Using integer division:
row / 3 * 3 = 4 / 3 * 3 = 1 * 3 = 3(sub-grid starts at row 3)col / 3 * 3 = 7 / 3 * 3 = 2 * 3 = 6(sub-grid starts at column 6)
As i ranges from 0 to 8:
i / 3produces 0, 0, 0, 1, 1, 1, 2, 2, 2 (row offsets)i % 3produces 0, 1, 2, 0, 1, 2, 0, 1, 2 (column offsets)
This visits cells (3,6), (3,7), (3,8), (4,6), (4,7), (4,8), (5,6), (5,7), (5,8), which is exactly the 3x3 block containing (4,7).
Completed Board
After the solver runs on our example puzzle, the first three rows look like this. Blue cells were given, and orange cells were filled by the algorithm:
Loading visualization...
Complexity Analysis
Time Complexity: O((9!)^9)
In the theoretical worst case, each of the 9 rows could have up to 9! valid permutations. Since there are 9 rows, the upper bound is O((9!)^9). In practice, constraint checking prunes the vast majority of branches. Most standard Sudoku puzzles with 25+ givens solve in under a millisecond because each placement eliminates candidates from roughly 20 other cells (the row, column, and sub-grid peers).
Space Complexity: O(1)
The board is modified in-place with a fixed size of 81 cells. The recursion stack depth is bounded by the number of empty cells, which is at most 81. Since both the board dimensions and recursion depth are constants (independent of any input parameter), the space complexity is O(1).
Alternative Approaches
- Constraint propagation with backtracking: Reduce candidates for each cell before guessing. This is what expert Sudoku players do mentally and dramatically cuts the search space.
- Dancing Links (DLX): Treats Sudoku as an exact cover problem. Extremely fast but complex to implement and not expected in interviews.
- Bit manipulation: Use bitmasks to track which digits are used in each row, column, and sub-grid. Turns validation into O(1) bitwise operations. A good optimization to mention as a follow-up.
Common Pitfalls
-
Forgetting to backtrack: The line
board[row][col] = '.'is essential. Without it, incorrect guesses remain on the board and break constraint checks for other cells. -
Returning the wrong value: After trying all 9 digits for an empty cell, you must
return false. Falling through without returning causes the solver to skip empty cells and produce invalid boards. -
Wrong sub-grid formula: Using
row % 3instead ofrow / 3 * 3computes the offset within the sub-grid rather than the sub-grid's starting position. This is the most common bug in interviews. -
Modifying the board without returning it: The problem asks you to return the board even though it's modified in-place. Forgetting the return statement is an easy mistake.
Interview Tips
When solving this problem live:
-
Start with the structure: Explain the three Sudoku constraints and the backtracking strategy before writing any code. Interviewers want to see your thought process.
-
Write
isValidfirst: It's the simpler function and establishes the constraint-checking foundation. Then buildsolveon top of it. -
Walk through the sub-grid formula: This is the part interviewers ask about most. Draw a 9x9 grid, pick a cell, and show how the formula visits the correct 3x3 block.
-
Mention optimizations: After presenting the basic solution, discuss how bitmasks or constraint propagation could speed things up. This shows depth beyond the textbook answer.
-
Relate to other backtracking problems: If you've solved N-Queens or word search, point out the shared "try-validate-recurse-backtrack" pattern.
Key Takeaways
- The backtracking template for Sudoku (find empty cell, try digits, validate, recurse, reset on failure) is the standard interview solution at companies like Bloomberg, Microsoft, and Google.
- The sub-grid formula
row / 3 * 3 + i / 3andcol / 3 * 3 + i % 3lets you check all 9 cells of a 3x3 block in a single loop alongside the row and column checks. - Always reset the cell to
'.'when backtracking. This single line is the difference between a correct solver and one that produces garbage. - Real-world Sudoku solving uses constraint propagation to reduce candidates before guessing. Mention this as an optimization if time permits.
- The same "try, validate, recurse, backtrack" pattern powers solutions for N-Queens, permutation generation, combination sum, and word search problems.
Related Problems and Practice
Once you've nailed the Sudoku solver, these related problems build on the same backtracking foundation:
- Sudoku Board Integrity Check (Problem 183): Validate a partially filled board without solving it. A simpler variant that focuses only on constraint checking.
- N-Queens: Place N queens on an N x N board with row, column, and diagonal constraints.
- Word Search / Boggle Word Finder: Search for words in a grid using DFS with backtracking.
- Generate Combinations of Parentheses: Build valid strings character by character, backtracking on invalid states.
This problem and hundreds of others are available on Firecode, where over 50,000 engineers have prepared for technical interviews and landed roles at top companies. Whether you're tackling your first backtracking problem or refining your constraint satisfaction technique, consistent practice is what separates candidates who pass from those who don't.
Solutions in Other Languages
Python
class Solution:
def solve_sudoku(self, board):
def is_valid(board, row, col, num):
for i in range(9):
if board[row][i] == num or board[i][col] == num:
return False
if board[3 * (row // 3) + i // 3][3 * (col // 3) + i % 3] == num:
return False
return True
def solve():
for i in range(9):
for j in range(9):
if board[i][j] == '.':
for num in map(str, range(1, 10)):
if is_valid(board, i, j, num):
board[i][j] = num
if solve():
return True
board[i][j] = '.'
return False
return True
solve()
return board
JavaScript
class Solution {
solveSudoku(board) {
const isValid = (board, row, col, num) => {
for (let i = 0; i < 9; i++) {
if (board[row][i] === num || board[i][col] === num ||
board[Math.floor(row / 3) * 3 + Math.floor(i / 3)][Math.floor(col / 3) * 3 + i % 3] === num) {
return false;
}
}
return true;
};
const solve = () => {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (board[row][col] === '.') {
for (let num = 1; num <= 9; num++) {
const charNum = num.toString();
if (isValid(board, row, col, charNum)) {
board[row][col] = charNum;
if (solve()) return true;
board[row][col] = '.';
}
}
return false;
}
}
}
return true;
};
solve();
return board;
}
}
TypeScript
class Solution {
solveSudoku(board: string[][]): string[][] {
const isValid = (board: string[][], row: number, col: number, num: string): boolean => {
for (let i = 0; i < 9; i++) {
if (board[row][i] === num || board[i][col] === num ||
board[Math.floor(row / 3) * 3 + Math.floor(i / 3)][Math.floor(col / 3) * 3 + i % 3] === num) {
return false;
}
}
return true;
};
const solve = (): boolean => {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (board[row][col] === '.') {
for (let num = 1; num <= 9; num++) {
const charNum = num.toString();
if (isValid(board, row, col, charNum)) {
board[row][col] = charNum;
if (solve()) return true;
board[row][col] = '.';
}
}
return false;
}
}
}
return true;
};
solve();
return board;
}
}
C++
#include <vector>
class Solution {
public:
std::vector<std::vector<char>> solveSudoku(std::vector<std::vector<char>> &board) {
solve(board);
return board;
}
private:
bool solve(std::vector<std::vector<char>> &board) {
for (int row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) {
if (board[row][col] == '.') {
for (char num = '1'; num <= '9'; ++num) {
if (isValid(board, row, col, num)) {
board[row][col] = num;
if (solve(board)) return true;
board[row][col] = '.';
}
}
return false;
}
}
}
return true;
}
bool isValid(const std::vector<std::vector<char>> &board, int row, int col, char num) {
for (int i = 0; i < 9; ++i) {
if (board[row][i] == num || board[i][col] == num ||
board[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3] == num) {
return false;
}
}
return true;
}
};
Scala
class Solution {
def solveSudoku(board: Array[Array[Char]]): Array[Array[Char]] = {
def isValid(board: Array[Array[Char]], row: Int, col: Int, num: Char): Boolean = {
for (i <- 0 until 9) {
if (board(row)(i) == num || board(i)(col) == num ||
board(3 * (row / 3) + i / 3)(3 * (col / 3) + i % 3) == num) {
return false
}
}
true
}
def solve(board: Array[Array[Char]]): Boolean = {
for (row <- 0 until 9) {
for (col <- 0 until 9) {
if (board(row)(col) == '.') {
for (num <- '1' to '9') {
if (isValid(board, row, col, num)) {
board(row)(col) = num
if (solve(board)) return true
board(row)(col) = '.'
}
}
return false
}
}
}
true
}
solve(board)
board
}
}
Kotlin
class Solution {
fun solveSudoku(board: Array<CharArray>): Array<CharArray> {
solve(board)
return board
}
private fun solve(board: Array<CharArray>): Boolean {
for (row in 0 until 9) {
for (col in 0 until 9) {
if (board[row][col] == '.') {
for (num in '1'..'9') {
if (isValid(board, row, col, num)) {
board[row][col] = num
if (solve(board)) return true
board[row][col] = '.'
}
}
return false
}
}
}
return true
}
private fun isValid(board: Array<CharArray>, row: Int, col: Int, num: Char): Boolean {
for (i in 0 until 9) {
if (board[row][i] == num || board[i][col] == num ||
board[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3] == num) {
return false
}
}
return true
}
}
Ruby
class Solution
def solve_sudoku(board)
solve(board)
board
end
def solve(board)
(0...9).each do |row|
(0...9).each do |col|
if board[row][col] == '.'
('1'..'9').each do |num|
if valid?(board, row, col, num)
board[row][col] = num
return true if solve(board)
board[row][col] = '.'
end
end
return false
end
end
end
true
end
def valid?(board, row, col, num)
(0...9).each do |i|
if board[row][i] == num || board[i][col] == num ||
board[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3] == num
return false
end
end
true
end
end
Swift
class Solution {
func solveSudoku(_ board: [[Character]]) -> [[Character]] {
var board = board
solve(&board)
return board
}
private func solve(_ board: inout [[Character]]) -> Bool {
for row in 0..<9 {
for col in 0..<9 {
if board[row][col] == Character(".") {
for num in "123456789" {
if isValid(board, row, col, num) {
board[row][col] = num
if solve(&board) { return true }
board[row][col] = Character(".")
}
}
return false
}
}
}
return true
}
private func isValid(_ board: [[Character]], _ row: Int, _ col: Int, _ num: Character) -> Bool {
for i in 0..<9 {
if board[row][i] == num || board[i][col] == num ||
board[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3] == num {
return false
}
}
return true
}
}
Rust
impl Solution {
pub fn solve_sudoku(&self, mut board: Vec<Vec<char>>) -> Vec<Vec<char>> {
Self::solve(&mut board);
board
}
fn solve(board: &mut Vec<Vec<char>>) -> bool {
for row in 0..9 {
for col in 0..9 {
if board[row][col] == '.' {
for num in '1'..='9' {
if Self::is_valid(board, row, col, num) {
board[row][col] = num;
if Self::solve(board) { return true; }
board[row][col] = '.';
}
}
return false;
}
}
}
true
}
fn is_valid(board: &[Vec<char>], row: usize, col: usize, num: char) -> bool {
for i in 0..9 {
if board[row][i] == num || board[i][col] == num ||
board[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3] == num {
return false;
}
}
true
}
}
C#
public class Solution {
public char[][] SolveSudoku(char[][] board) {
Solve(board);
return board;
}
private bool Solve(char[][] board) {
for (int row = 0; row < 9; row++) {
for (int col = 0; col < 9; col++) {
if (board[row][col] == '.') {
for (char num = '1'; num <= '9'; num++) {
if (IsValid(board, row, col, num)) {
board[row][col] = num;
if (Solve(board)) return true;
board[row][col] = '.';
}
}
return false;
}
}
}
return true;
}
private bool IsValid(char[][] board, int row, int col, char num) {
for (int i = 0; i < 9; i++) {
if (board[row][i] == num || board[i][col] == num ||
board[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3] == num) {
return false;
}
}
return true;
}
}
Dart
class Solution {
List<List<String>> solveSudoku(List<List<String>> board) {
_solve(board);
return board;
}
bool _solve(List<List<String>> board) {
for (int row = 0; row < 9; row++) {
for (int col = 0; col < 9; col++) {
if (board[row][col] == '.') {
for (int num = 1; num <= 9; num++) {
String numStr = num.toString();
if (_isValid(board, row, col, numStr)) {
board[row][col] = numStr;
if (_solve(board)) return true;
board[row][col] = '.';
}
}
return false;
}
}
}
return true;
}
bool _isValid(List<List<String>> board, int row, int col, String num) {
for (int i = 0; i < 9; i++) {
if (board[row][i] == num || board[i][col] == num ||
board[row ~/ 3 * 3 + i ~/ 3][col ~/ 3 * 3 + i % 3] == num) {
return false;
}
}
return true;
}
}
PHP
class Solution {
public function solveSudoku(array $board): array {
$this->solve($board);
return $board;
}
private function solve(array &$board): bool {
for ($row = 0; $row < 9; $row++) {
for ($col = 0; $col < 9; $col++) {
if ($board[$row][$col] === '.') {
for ($num = 1; $num <= 9; $num++) {
$charNum = (string)$num;
if ($this->isValid($board, $row, $col, $charNum)) {
$board[$row][$col] = $charNum;
if ($this->solve($board)) return true;
$board[$row][$col] = '.';
}
}
return false;
}
}
}
return true;
}
private function isValid(array $board, int $row, int $col, string $num): bool {
for ($i = 0; $i < 9; $i++) {
if ($board[$row][$i] === $num || $board[$i][$col] === $num ||
$board[intdiv($row, 3) * 3 + intdiv($i, 3)][intdiv($col, 3) * 3 + $i % 3] === $num) {
return false;
}
}
return true;
}
}