Problem overview
The method isValidSudoku receives a 9 x 9 board, a string[][] where each cell is a digit character from '1' to '9' or '.' for an empty cell. It returns true when no digit appears twice in the same row, the same column or the same 3 x 3 box, and false otherwise.
Only the filled cells are checked. The puzzle does not have to be solvable: a board with just the digits 1 to 8 in the top row and a 9 in the fifth row is valid, even though no one could finish it. The task is to inspect what is already on the board, not to solve it.
The well-known example board starting with 53..7.... gives true. Changing its first digit from 5 to 8 gives false, because column 0 then holds two 8s (rows 0 and 3), and so does the top left box (rows 0 and 2).
Approach: remember what each row, column and box has seen
You could scan the row, the column and the box of every cell again each time. On a board this small that works, but it looks at each cell many times. A single pass is simpler: visit the cells once, and keep a record of the digits seen so far in every row, every column and every box.
That record is a Set of digits for each of the 9 rows, 9 columns and 9 boxes: 27 sets in all. For each filled cell you ask three questions, and each answer takes constant time with has: is this digit already in my row's set, my column's set, or my box's set? If any answer is yes, the digit repeats and the board is invalid. Otherwise you add the digit to all three sets and move on. If the loops finish without a repeat, the board is valid.
The only tricky part is finding a cell's box. The boxes are numbered left to right, then top to bottom, from 0 to 8. The cell at row r, column c is in box row Math.floor(r / 3) and box column Math.floor(c / 3), so its box number is the box row times 3 plus the box column. For example, the cells (0, 0), (4, 7), (8, 8) and (3, 5) are in boxes 0, 5, 8 and 4.
Step by step
- Create the row sets.
rowsis an array of 9 emptySet<string>objects, built withArray.fromand an arrow function that creates a new set for each place.rows[r]then holds the digits seen so far in rowr. - Create
colsandboxesthe same way. - Loop over the rows with
rfrom 0 to 8. - Loop over the columns of that row with
cfrom 0 to 8, so every cell is visited, row by row. - Read the cell into a
constnameddigit. - Skip empty cells with
continuewhen the digit is'.'. Empty cells never break a rule. - Compute the box number in a
constnamedbox. - Look for a repeat. If the row set, the column set or the box set already has the digit, return
falseright away. - Record the digit in the row set.
- Record it in the column and box sets too.
- Return
trueafter both loops: every filled cell was compared with the ones before it, and none repeated.
The solution in TypeScript
class Solution {
isValidSudoku(board: string[][]): boolean {
const rows: Set<string>[] = Array.from({ length: 9 }, () => new Set<string>());
const cols: Set<string>[] = Array.from({ length: 9 }, () => new Set<string>());
const boxes: Set<string>[] = Array.from({ length: 9 }, () => new Set<string>());
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
const digit = board[r][c];
if (digit === '.') continue;
const box = Math.floor(r / 3) * 3 + Math.floor(c / 3);
if (rows[r].has(digit) || cols[c].has(digit) || boxes[box].has(digit)) {
return false;
}
rows[r].add(digit);
cols[c].add(digit);
boxes[box].add(digit);
}
}
return true;
}
}
Details that matter in TypeScript:
- Why
Array.fromwith a function.new Array(9).fill(new Set())looks shorter but creates one set and puts that same set in all 9 places. Adding a 5 torows[0]would then also makerows[8].has('5')true. The arrow function passed toArray.fromruns once per place, so each place gets its own set. - The type annotation on the sets.
Set<string>[]says that each set holds strings, andboard[r][c]is astring. Annotations are erased when the code is compiled to JavaScript, so they only help the compiler catch mistakes such as adding a number to a set of strings. - Digits stay characters. The cells are strings such as
'5'. There is no need to convert them to numbers: a set of strings works just as well, and'.'can be tested directly with===. Math.floorand division. In JavaScript and TypeScript,/keeps the fraction:7 / 3is 2.3333333333333335. WithoutMath.floor, the box number would be a fractional value and could never match an array position.if (digit === '.') continue;without braces is legal for a single statement.continuejumps to the next value ofc.- Short-circuit
||. The threehaschecks stop at the first one that is true. The result is the same either way, since the checks have no side effects.
Complexity
Time: O(81), which is constant. The loops visit each of the 81 cells once, and each visit does at most three has and three add calls on sets, which take constant time on average.
Space: O(81), also constant. The 27 sets together hold at most 81 digits, because each cell adds a digit to exactly one row set, one column set and one box set, and a set holds at most 9 distinct digits.
Edge cases and common mistakes
- An empty board. Every cell is skipped, so the method returns
true. An empty board does not break any rule. - A repeat in only one of the three groups. Two 5s in row 0 (columns 0 and 8) are caught by the row set. Two 5s in column 0 (rows 0 and 8) are caught by the column set. Two 5s at (0, 0) and (2, 2) are in different rows and columns, and only the box set catches them. Each of those boards makes the method return
false, so all three checks are needed. - The same digit in unrelated places. A 5 at (0, 0) and another at (3, 3) share no row, column or box, so the board stays valid.
- Treating
'.'as a digit. Without thecontinue, the second empty cell in a row would look like a repeat and the method would reject almost every board. - A wrong box formula. Using
r / 3 + c / 3mixes the row and the column in the same fractional number. Row times 3 plus column, after rounding each down, gives a different number for every box. - Solving the puzzle. The method is not asked to find out whether the board has a solution, only whether the digits placed break a rule. The board with 1 to 8 in the top row and a 9 at row 4, column 8 is valid, and the method returns
true.
Testing
Results from running the method:
- The board starting with
53..7....givestrue. - The same board with the first digit changed to 8 gives
false. - The board with
12345678.in the top row and a single 9 at the end of the fifth row givestrue. - A board with nothing but
.givestrue. - Two 5s in one row, in one column or in one box each give
false. - A 5 at (0, 0) and another at (3, 3) gives
true.
