Validate a partially-filled 9×9 Sudoku board without solving it. The trick is a single pass using three groups of hash sets— one per row, one per column, and one per 3×3 box — so each digit is checked and recorded in O(1) per cell.
Given a 9×9 board of digits 1–9 and '.' (empty), return trueif the board is valid — that is, no digit appears twice in any row, column, or 3×3 sub-box.
Concrete example. The top-left 3×3 sub-box contains 5, 3, 6, 9, 8 — all distinct. Row 1 is 5 3 . . 7 . . . . — again all distinct digits. The standard LeetCode example board returns true. If you instead put a second 8 in row 1, it returns false immediately.
You do notneed to solve or complete the board — only check whether what's already filled violates the rules.
false; otherwise add it to all three. One O(81) pass, three O(1) lookups per cell.Set<string> — rows[0..8], cols[0..8], boxes[0..8]. One set per unit. This is just nine buckets per dimension.r 0→8, c 0→8. For each cell, skip immediately if val === '.'.boxIdx = Math.floor(r/3)*3 + Math.floor(c/3). This maps any (r, c) to an index 0–8 uniquely identifying its 3×3 sub-grid — the key insight that avoids storing a 2D array of sets.rows[r], cols[c], or boxes[boxIdx] already has val, return false. Otherwise .add(val)to all three. Check before insert so you never mark the duplicate as "seen."Math.floor(r/3)*3 + Math.floor(c/3), which maps the 3×3 sub-grid coordinates to a flat index 0–8. A common mistake is using r*3+c (that gives 0–80, not 0–8) or forgetting the *3 multiplier. Write it out for cell (4,7): Math.floor(4/3)*3 + Math.floor(7/3) = 1*3+2 = 5— box 5, which is the middle-right 3×3. Correct.The board is always 9×9 so both are technically O(1) (bounded constants), but the hash-set approach is O(1) per cell vs O(81) per cell — a 81× practical improvement and the pattern that scales to arbitrary N×N Sudoku.
Alternative — bit masking: replace each Set<string> with a number bitmask (bit k set = digit k+1 seen). Same O(1) logic but avoids object allocation.
1▶function isValidSudoku(board: string[][]): boolean {2▶ const rows = Array.from({ length: 9 }, () => new Set<string>());3▶ const cols = Array.from({ length: 9 }, () => new Set<string>());4▶ const boxes = Array.from({ length: 9 }, () => new Set<string>());56 for (let r = 0; r < 9; r++) {7 for (let c = 0; c < 9; c++) {8 const val = board[r][c];9 if (val === '.') continue;1011 const boxIdx = Math.floor(r / 3) * 3 + Math.floor(c / 3);1213 if (rows[r].has(val) || cols[c].has(val) || boxes[boxIdx].has(val)) {14 return false;15 }1617 rows[r].add(val);18 cols[c].add(val);19 boxes[boxIdx].add(val);20 }21 }22 return true;23}
function isValidSudoku(board: string[][]): boolean {
const rows = Array.from({ length: 9 }, () => new Set<string>());
const cols = Array.from({ length: 9 }, () => new Set<string>());
const boxes = Array.from({ length: 9 }, () => new Set<string>());
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
const val = board[r][c];
if (val === '.') continue;
const boxIdx = Math.floor(r / 3) * 3 + Math.floor(c / 3);
if (rows[r].has(val) || cols[c].has(val) || boxes[boxIdx].has(val)) {
return false;
}
rows[r].add(val);
cols[c].add(val);
boxes[boxIdx].add(val);
}
}
return true;
}Array.from({ length: 9 }, () => new Set())creates nine independent sets for rows, nine for columns, and nine for 3×3 boxes. This is the only setup; the rest is a single scan.r is the row index (0–8), c is the column index (0–8). Every cell is visited exactly once.Math.floor(r/3)*3 + Math.floor(c/3) maps any (r, c) to one of nine sub-grid indices 0–8. This is the load-bearing formula — memorize it.|| expression tests all three sets simultaneously. If the digit already exists in any of them, the board is invalid and we return false immediately — no need to finish scanning.Set<string> with a number bitmask. Bit k = digit k+1 seen. Check with mask & (1 << digit); insert with mask |= (1 << digit). Same O(1) logic, zero object allocation.Math.floor(r/k)*k + Math.floor(c/k)where k = √N. Sets become O(N) in size; overall O(N²) time and space.function isValidSudoku(board: string[][]): boolean {
const rows = Array.from({ length: 9 }, () => new Set<string>());
const cols = Array.from({ length: 9 }, () => new Set<string>());
const boxes = Array.from({ length: 9 }, () => new Set<string>());
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
const val = board[r][c];
if (val === '.') continue;
const boxIdx = Math.floor(r / 3) * 3 + Math.floor(c / 3);
if (rows[r].has(val) || cols[c].has(val) || boxes[boxIdx].has(val)) {
return false;
}
rows[r].add(val);
cols[c].add(val);
boxes[boxIdx].add(val);
}
}
return true;
}Check every row, every column, and every 3×3 box independently: for each of the 27 units, collect its non-dot digits and look for a duplicate with a nested comparison — no persistent hash sets carried across the board.
function isValidSudoku(board: string[][]): boolean {
// Returns true if the nine cells given by (r, c) pairs have no repeated digit.
function unitOk(cells: [number, number][]): boolean {
const seen: string[] = [];
for (const [r, c] of cells) {
const val = board[r][c];
if (val === '.') continue;
// Linear scan against everything seen so far in THIS unit.
for (const prev of seen) {
if (prev === val) return false;
}
seen.push(val);
}
return true;
}
for (let i = 0; i < 9; i++) {
// Row i, then column i.
const row: [number, number][] = [];
const col: [number, number][] = [];
for (let k = 0; k < 9; k++) {
row.push([i, k]);
col.push([k, i]);
}
if (!unitOk(row) || !unitOk(col)) return false;
}
// Each of the nine 3×3 boxes.
for (let br = 0; br < 9; br += 3) {
for (let bc = 0; bc < 9; bc += 3) {
const box: [number, number][] = [];
for (let dr = 0; dr < 3; dr++) {
for (let dc = 0; dc < 3; dc++) {
box.push([br + dr, bc + dc]);
}
}
if (!unitOk(box)) return false;
}
}
return true;
}| "validate a grid without solving it" | hash sets per row/col/sub-grid |
| map (r, c) to a 3×3 region | Math.floor(r/3)*3 + Math.floor(c/3) |
| "detect duplicates in 2D partitions" | flat-indexed set families |
| "Sudoku Solver / backtracking on board" | reuse these three set families as constraints |
const rows = Array.from({ length: 9 }, () => new Set<string>());
const cols = Array.from({ length: 9 }, () => new Set<string>());
const boxes = Array.from({ length: 9 }, () => new Set<string>());
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
const val = board[r][c];
if (val === '.') continue;
const boxIdx = Math.floor(r / 3) * 3 + Math.floor(c / 3);
if (rows[r].has(val) || cols[c].has(val) || boxes[boxIdx].has(val)) return false;
rows[r].add(val); cols[c].add(val); boxes[boxIdx].add(val);
}
}
return true;(r=4, c=7)?'8' in row 5 at column 2 and again at column 7. When does the algorithm return false?