36. Valid Sudoku
Problem Description
You need to determine if a given 9×9 Sudoku board is valid. The board is represented as a 2D array where each cell contains either a digit from '1'
to '9'
or a '.'
(representing an empty cell).
A valid Sudoku board must satisfy three rules:
- Each row must contain the digits 1-9 without any repetition (considering only filled cells)
- Each column must contain the digits 1-9 without any repetition (considering only filled cells)
- Each of the nine 3×3 sub-boxes must contain the digits 1-9 without any repetition (considering only filled cells)
Important points to note:
- You only need to validate the filled cells according to these rules - empty cells (marked with
'.'
) are ignored - A board can be valid even if it's not completely filled
- A valid board doesn't need to be solvable - it just needs to follow the rules for the numbers that are already placed
For example, if a row contains ['5', '.', '.', '3', '.', '.', '.', '.', '5']
, this would be invalid because the digit '5'
appears twice in the same row.
The 3×3 sub-boxes are the nine non-overlapping squares that divide the 9×9 board:
- Top-left box: rows 0-2, columns 0-2
- Top-middle box: rows 0-2, columns 3-5
- Top-right box: rows 0-2, columns 6-8
- And so on for the middle and bottom rows
Your task is to return true
if the board configuration is valid according to these rules, or false
otherwise.
Intuition
The key insight is that we need to track which numbers have already appeared in each row, column, and 3×3 sub-box. If we encounter a number that has already been seen in its respective row, column, or sub-box, we know the board is invalid.
Think of it like having three separate "record books":
- One for tracking which numbers appear in each row
- One for tracking which numbers appear in each column
- One for tracking which numbers appear in each 3×3 sub-box
As we scan through the board cell by cell, for each filled cell we check: "Has this number already appeared in this row? In this column? In this 3×3 sub-box?" If the answer to any of these questions is "yes", we've found a violation and can immediately return false
.
The tricky part is mapping each cell to its corresponding 3×3 sub-box. We can think of the nine sub-boxes as being arranged in a 3×3 grid themselves (sub-box 0-8). For a cell at position (i, j)
:
- The sub-box row is
i // 3
(integer division) - The sub-box column is
j // 3
- The sub-box index can be calculated as
(i // 3) * 3 + (j // 3)
For example, cell (5, 7)
belongs to sub-box (5//3)*3 + (7//3) = 1*3 + 2 = 5
.
We use boolean arrays to track whether each digit (1-9) has appeared in each row, column, and sub-box. Since we're using 0-based indexing, digit d
is stored at index d-1
. This gives us an efficient O(1) lookup to check if a number has been seen before.
The beauty of this approach is that we only need to traverse the board once, checking and marking as we go. If we complete the traversal without finding any conflicts, the board must be valid.
Solution Approach
We implement the solution using a single traversal approach with three 2D boolean arrays to track the appearance of digits.
Data Structures:
row
: A 9×9 boolean array whererow[i][num]
indicates whether digitnum+1
has appeared in rowi
col
: A 9×9 boolean array wherecol[j][num]
indicates whether digitnum+1
has appeared in columnj
sub
: A 9×9 boolean array wheresub[k][num]
indicates whether digitnum+1
has appeared in sub-boxk
Algorithm Steps:
-
Initialize tracking arrays: Create three 9×9 boolean arrays initialized to
False
. Each array has 9 rows (one for each row/column/sub-box) and 9 columns (one for each possible digit 1-9). -
Traverse the board: Use nested loops to visit each cell
(i, j)
in the 9×9 board. -
Skip empty cells: If the current cell contains
'.'
, continue to the next cell. -
Process filled cells:
- Convert the character digit to its numeric index:
num = int(c) - 1
(subtract 1 for 0-based indexing) - Calculate the sub-box index:
k = (i // 3) * 3 + (j // 3)
i // 3
gives the sub-box row (0, 1, or 2)j // 3
gives the sub-box column (0, 1, or 2)- The formula maps to sub-boxes 0-8 in row-major order
- Convert the character digit to its numeric index:
-
Check for violations: Before marking the digit as seen, check if it already exists:
row[i][num]
: Has this digit appeared in rowi
?col[j][num]
: Has this digit appeared in columnj
?sub[k][num]
: Has this digit appeared in sub-boxk
?
If any of these is
True
, returnFalse
immediately. -
Mark the digit as seen: If no violation is found, mark the digit in all three tracking arrays:
row[i][num] = True
col[j][num] = True
sub[k][num] = True
-
Return result: If the entire board is traversed without finding any violations, return
True
.
Time Complexity: O(1) - We always traverse a fixed 9×9 board, making 81 iterations.
Space Complexity: O(1) - We use three 9×9 arrays, which is a constant amount of space regardless of input.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small 4×4 simplified Sudoku example to illustrate the solution approach (using 2×2 sub-boxes instead of 3×3 for simplicity):
Board: [1, 2, ., 4] [3, 4, 1, .] [., 1, 4, 3] [4, 3, 2, 1]
Step 1: Initialize tracking arrays
row[4][4]
: all False (tracks digits 1-4 in each row)col[4][4]
: all False (tracks digits 1-4 in each column)sub[4][4]
: all False (tracks digits 1-4 in each 2×2 sub-box)
Step 2: Process cell (0,0) = '1'
- Digit index: 1-1 = 0
- Sub-box: (0//2)*2 + (0//2) = 0
- Check: row[0][0]? No. col[0][0]? No. sub[0][0]? No.
- Mark: row[0][0]=True, col[0][0]=True, sub[0][0]=True
Step 3: Process cell (0,1) = '2'
- Digit index: 2-1 = 1
- Sub-box: (0//2)*2 + (1//2) = 0
- Check: row[0][1]? No. col[1][1]? No. sub[0][1]? No.
- Mark: row[0][1]=True, col[1][1]=True, sub[0][1]=True
Step 4: Skip cell (0,2) = '.'
Step 5: Process cell (0,3) = '4'
- Digit index: 4-1 = 3
- Sub-box: (0//2)*2 + (3//2) = 1
- Check: row[0][3]? No. col[3][3]? No. sub[1][3]? No.
- Mark: row[0][3]=True, col[3][3]=True, sub[1][3]=True
Continue for row 1...
Step 8: Process cell (1,2) = '1'
- Digit index: 1-1 = 0
- Sub-box: (1//2)*2 + (2//2) = 1
- Check: row[1][0]? No. col[2][0]? No. sub[1][0]? No.
- Mark: row[1][0]=True, col[2][0]=True, sub[1][0]=True
Later: Process cell (2,1) = '1'
- Digit index: 1-1 = 0
- Sub-box: (2//2)*2 + (1//2) = 2
- Check:
- row[2][0]? No (digit 1 hasn't appeared in row 2 yet)
- col[1][0]? No (digit 1 hasn't appeared in column 1 yet)
- sub[2][0]? No (digit 1 hasn't appeared in sub-box 2 yet)
- Mark: row[2][0]=True, col[1][0]=True, sub[2][0]=True
The board is valid! Each digit appears at most once in each row, column, and sub-box.
Invalid Example: If we had:
[1, 1, ., .] <- Invalid: '1' appears twice in row 0
When processing cell (0,1) = '1':
- Digit index: 0
- Sub-box: 0
- Check: row[0][0]? Yes! (we already saw '1' in row 0)
- Return False immediately
This demonstrates how the algorithm efficiently detects violations by tracking seen digits in each constraint (row, column, sub-box).
Solution Implementation
1class Solution:
2 def isValidSudoku(self, board: List[List[str]]) -> bool:
3 # Initialize tracking arrays for rows, columns, and 3x3 sub-boxes
4 # Each inner array tracks whether digits 1-9 have been seen (index 0-8)
5 rows_seen = [[False] * 9 for _ in range(9)]
6 cols_seen = [[False] * 9 for _ in range(9)]
7 boxes_seen = [[False] * 9 for _ in range(9)]
8
9 # Iterate through each cell in the 9x9 board
10 for row_idx in range(9):
11 for col_idx in range(9):
12 cell_value = board[row_idx][col_idx]
13
14 # Skip empty cells
15 if cell_value == '.':
16 continue
17
18 # Convert digit character to 0-based index (1-9 becomes 0-8)
19 digit_idx = int(cell_value) - 1
20
21 # Calculate which 3x3 sub-box this cell belongs to
22 # Formula maps (row, col) to box index 0-8
23 box_idx = (row_idx // 3) * 3 + (col_idx // 3)
24
25 # Check if this digit already exists in current row, column, or box
26 if (rows_seen[row_idx][digit_idx] or
27 cols_seen[col_idx][digit_idx] or
28 boxes_seen[box_idx][digit_idx]):
29 return False
30
31 # Mark this digit as seen in the current row, column, and box
32 rows_seen[row_idx][digit_idx] = True
33 cols_seen[col_idx][digit_idx] = True
34 boxes_seen[box_idx][digit_idx] = True
35
36 # All cells are valid if we reach here
37 return True
38
1class Solution {
2 public boolean isValidSudoku(char[][] board) {
3 // Create 2D boolean arrays to track numbers in rows, columns, and sub-boxes
4 // First dimension: row/column/sub-box index (0-8)
5 // Second dimension: number value (0-8, representing digits 1-9)
6 boolean[][] rowHasNumber = new boolean[9][9];
7 boolean[][] columnHasNumber = new boolean[9][9];
8 boolean[][] subBoxHasNumber = new boolean[9][9];
9
10 // Iterate through each cell in the 9x9 board
11 for (int row = 0; row < 9; row++) {
12 for (int column = 0; column < 9; column++) {
13 char currentCell = board[row][column];
14
15 // Skip empty cells
16 if (currentCell == '.') {
17 continue;
18 }
19
20 // Convert character digit to 0-based index (e.g., '1' -> 0, '9' -> 8)
21 int digitIndex = currentCell - '0' - 1;
22
23 // Calculate sub-box index (0-8) based on current position
24 // Sub-boxes are numbered left-to-right, top-to-bottom
25 int subBoxIndex = (row / 3) * 3 + (column / 3);
26
27 // Check if this digit already exists in current row, column, or sub-box
28 if (rowHasNumber[row][digitIndex] ||
29 columnHasNumber[column][digitIndex] ||
30 subBoxHasNumber[subBoxIndex][digitIndex]) {
31 return false; // Duplicate found, invalid Sudoku
32 }
33
34 // Mark this digit as seen in the current row, column, and sub-box
35 rowHasNumber[row][digitIndex] = true;
36 columnHasNumber[column][digitIndex] = true;
37 subBoxHasNumber[subBoxIndex][digitIndex] = true;
38 }
39 }
40
41 // No duplicates found, valid Sudoku configuration
42 return true;
43 }
44}
45
1class Solution {
2public:
3 bool isValidSudoku(vector<vector<char>>& board) {
4 // Create 2D arrays to track numbers in rows, columns, and 3x3 sub-boxes
5 // rows[i][num] = true if number (num+1) exists in row i
6 vector<vector<bool>> rows(9, vector<bool>(9, false));
7 // columns[j][num] = true if number (num+1) exists in column j
8 vector<vector<bool>> columns(9, vector<bool>(9, false));
9 // subBoxes[k][num] = true if number (num+1) exists in sub-box k
10 vector<vector<bool>> subBoxes(9, vector<bool>(9, false));
11
12 // Iterate through each cell in the 9x9 board
13 for (int row = 0; row < 9; ++row) {
14 for (int col = 0; col < 9; ++col) {
15 char currentCell = board[row][col];
16
17 // Skip empty cells
18 if (currentCell == '.') {
19 continue;
20 }
21
22 // Convert character to 0-indexed number (e.g., '1' -> 0, '9' -> 8)
23 int digitIndex = currentCell - '0' - 1;
24
25 // Calculate sub-box index (0-8) based on current position
26 // Sub-boxes are numbered 0-8 from left to right, top to bottom
27 int subBoxIndex = (row / 3) * 3 + (col / 3);
28
29 // Check if this number already exists in the same row, column, or sub-box
30 if (rows[row][digitIndex] ||
31 columns[col][digitIndex] ||
32 subBoxes[subBoxIndex][digitIndex]) {
33 return false; // Invalid Sudoku: duplicate found
34 }
35
36 // Mark this number as seen in the current row, column, and sub-box
37 rows[row][digitIndex] = true;
38 columns[col][digitIndex] = true;
39 subBoxes[subBoxIndex][digitIndex] = true;
40 }
41 }
42
43 // All cells checked without duplicates found
44 return true;
45 }
46};
47
1/**
2 * Validates if a 9x9 Sudoku board is valid according to Sudoku rules.
3 * Only filled cells need to be validated.
4 *
5 * @param board - 9x9 2D array representing the Sudoku board, where '.' represents empty cells
6 * @returns true if the board is valid, false otherwise
7 */
8function isValidSudoku(board: string[][]): boolean {
9 // Track which numbers (1-9) have been used in each row
10 // rowUsed[i][num] = true means number (num+1) is used in row i
11 const rowUsed: boolean[][] = Array.from({ length: 9 }, () =>
12 Array.from({ length: 9 }, () => false)
13 );
14
15 // Track which numbers (1-9) have been used in each column
16 // columnUsed[j][num] = true means number (num+1) is used in column j
17 const columnUsed: boolean[][] = Array.from({ length: 9 }, () =>
18 Array.from({ length: 9 }, () => false)
19 );
20
21 // Track which numbers (1-9) have been used in each 3x3 sub-box
22 // subBoxUsed[k][num] = true means number (num+1) is used in sub-box k
23 const subBoxUsed: boolean[][] = Array.from({ length: 9 }, () =>
24 Array.from({ length: 9 }, () => false)
25 );
26
27 // Iterate through each cell in the board
28 for (let rowIndex = 0; rowIndex < 9; ++rowIndex) {
29 for (let colIndex = 0; colIndex < 9; ++colIndex) {
30 // Convert character to number index (0-8 for digits 1-9)
31 const numberIndex = board[rowIndex][colIndex].charCodeAt(0) - '1'.charCodeAt(0);
32
33 // Skip empty cells (represented by '.')
34 if (numberIndex < 0 || numberIndex > 8) {
35 continue;
36 }
37
38 // Calculate which 3x3 sub-box this cell belongs to (0-8)
39 // Sub-boxes are numbered left-to-right, top-to-bottom
40 const subBoxIndex = Math.floor(rowIndex / 3) * 3 + Math.floor(colIndex / 3);
41
42 // Check if this number already exists in the same row, column, or sub-box
43 if (rowUsed[rowIndex][numberIndex] ||
44 columnUsed[colIndex][numberIndex] ||
45 subBoxUsed[subBoxIndex][numberIndex]) {
46 return false;
47 }
48
49 // Mark this number as used in the current row, column, and sub-box
50 rowUsed[rowIndex][numberIndex] = true;
51 columnUsed[colIndex][numberIndex] = true;
52 subBoxUsed[subBoxIndex][numberIndex] = true;
53 }
54 }
55
56 return true;
57}
58
Time and Space Complexity
The time complexity is O(1)
and the space complexity is O(1)
.
Although the code contains nested loops that iterate through the 9×9 board, the board size is fixed at 81 cells. The algorithm examines each cell exactly once, performing constant-time operations (checking and updating boolean arrays) for each non-empty cell. Since the board dimensions are constant (9×9), the total number of operations is bounded by a constant value of 81.
For space complexity, the algorithm uses three 9×9 boolean arrays (row
, col
, and sub
) to track which numbers have been used in each row, column, and 3×3 sub-box respectively. This results in 3 × 9 × 9 = 243 boolean values, which is a constant amount of space regardless of input.
The reference answer states O(C)
where C = 81
, which effectively equals O(81) = O(1)
since 81 is a constant. The complexity doesn't grow with input size because the Sudoku board dimensions are fixed.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Sub-box Index Calculation
One of the most common mistakes is incorrectly calculating which 3×3 sub-box a cell belongs to. Developers often confuse the formula or use incorrect operations.
Incorrect approaches:
- Using
box_idx = (row_idx % 3) * 3 + (col_idx % 3)
- This gives the position within a box, not the box index - Using
box_idx = row_idx // 3 + col_idx // 3
- This doesn't properly enumerate the 9 boxes - Forgetting integer division and using regular division
/
instead of//
Correct approach:
box_idx = (row_idx // 3) * 3 + (col_idx // 3)
This formula works because:
row_idx // 3
gives you which row of boxes (0, 1, or 2)col_idx // 3
gives you which column of boxes (0, 1, or 2)- Multiplying the row by 3 and adding the column gives boxes numbered 0-8
2. Off-by-One Errors with Digit Indexing
Since the board contains digits '1' through '9' as characters, but array indices are 0-based, forgetting to adjust the index is a frequent error.
Incorrect:
digit_idx = int(cell_value) # Wrong! This would try to access index 9 for digit '9'
Correct:
digit_idx = int(cell_value) - 1 # Converts '1'-'9' to indices 0-8
3. Using Sets Incorrectly
While using sets is a valid alternative approach, a common mistake is creating the wrong number of sets or using string concatenation incorrectly.
Problematic approach:
seen = set()
for i in range(9):
for j in range(9):
if board[i][j] != '.':
val = board[i][j]
# These string keys must be unique and consistent
if f"row{i}{val}" in seen: # Easy to mess up the format
return False
seen.add(f"row{i}{val}")
# ... similar for column and box
Better set-based approach:
seen = set()
for i in range(9):
for j in range(9):
if board[i][j] != '.':
val = board[i][j]
box_idx = (i // 3) * 3 + (j // 3)
# Use tuples for clarity and type safety
row_key = ('row', i, val)
col_key = ('col', j, val)
box_key = ('box', box_idx, val)
if row_key in seen or col_key in seen or box_key in seen:
return False
seen.add(row_key)
seen.add(col_key)
seen.add(box_key)
4. Checking for Complete Solution Instead of Validity
Some developers mistakenly try to verify if the board is completely filled or solvable, rather than just checking if the current configuration is valid.
Wrong interpretation:
# Checking if every row has all digits 1-9 for row in board: if '.' in row: return False # Wrong! Empty cells are allowed
Correct interpretation: The problem only asks to validate that filled cells don't violate Sudoku rules. Empty cells should be ignored, and the board doesn't need to be complete or solvable.
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!