Problem overview
The function is_valid_sudoku receives a 9 x 9 board as a Vec<Vec<char>>. Each cell holds a digit from '1' to '9', or '.' when it is empty. The board is valid when no digit appears twice in the same row, the same column or the same 3 x 3 box. Only filled cells count, and the board does not have to be solvable: the function only checks the digits that are already there.
The first example is a partly filled board with no repeated digit, so the result is true. Replace its top-left 5 with an 8 and the result is false, because column 0 now holds two 8s (rows 0 and 3) and so does the top-left box.
The third example shows why "valid" is not "solvable". Row 0 holds the digits 1 to 8 and a lone 9 sits in row 4, column 8. The last cell of row 0 could then only be a 9, but column 8 already has one, so the puzzle has no solution. No digit repeats, though, so the function returns true.
Approach: remember what each row, column and box has seen
The slow way rescans the row, the column and the box of every cell. The lesson asks for one visit per cell with a constant-time check. For that, keep a set of seen digits for each of the 9 rows, each of the 9 columns and each of the 9 boxes, 27 sets in all. Every cell belongs to exactly one row, one column and one box. When a filled cell is visited:
- If its digit is already in any of its three sets, it is a repeat, so return
false. - Otherwise add the digit to all three sets and go on.
Each cell is compared with the cells visited before it, so every pair of cells that shares a row, column or box is compared exactly once.
Numbering the boxes
The cell at row r and column c lies in box row r / 3 and box column c / 3. Number the boxes left to right and then top to bottom, and the box index is (r / 3) * 3 + c / 3. Integer division drops the remainder, so 7 / 3 is 2, not 2.33. Some examples:
cell (0,0) box 0
cell (2,3) box 1
cell (4,7) box 5
cell (8,8) box 8
Step by step
- Create the row sets.
rowsis aVec<HashSet<char>>of nine empty sets, built withvec![HashSet::new(); 9].rows[r]holds the digits seen so far in rowr. - Create
colsandboxesthe same way: 27 sets in total, one per row, column and box. - Visit every cell row by row with an outer loop for
rover0..9and an inner loop forcover0..9. - Read the cell into
digit. - Skip empty cells. If
digit == '.',continueto the next cell. Empty cells never break a rule. - Compute
box_indexwith the formula above. The name avoidsbox, which is a reserved word in Rust. - Look for a repeat. If
rows[r],cols[c]orboxes[box_index]already contains the digit, returnfalse.containstakes a reference, so pass&digit. - Record the digit in all three sets with
insert, so later cells in the same row, column or box can find it. - Finish with
trueafter both loops, without a semicolon. Every filled cell was checked against the ones before it and none repeated.
The solution in Rust
impl Solution {
fn is_valid_sudoku(board: Vec<Vec<char>>) -> bool {
let mut rows: Vec<HashSet<char>> = vec![HashSet::new(); 9];
let mut cols: Vec<HashSet<char>> = vec![HashSet::new(); 9];
let mut boxes: Vec<HashSet<char>> = vec![HashSet::new(); 9];
for r in 0..9 {
for c in 0..9 {
let digit = board[r][c];
if digit == '.' {
continue;
}
let box_index = (r / 3) * 3 + c / 3;
if rows[r].contains(&digit) || cols[c].contains(&digit) || boxes[box_index].contains(&digit) {
return false;
}
rows[r].insert(digit);
cols[c].insert(digit);
boxes[box_index].insert(digit);
}
}
true
}
}
The Rust details worth a closer look:
vec![HashSet::new(); 9]clones. Thevec![value; n]form makesnclones of the value, so it needs a type that implementsClone, andHashSetdoes. Each clone is a separate set: inserting into the first one leaves the others empty. Afterv[0].insert('x')on such a vector of three sets, the sizes are 1, 0 and 0.- The type annotations.
HashSet::new()alone doesn't say what the sets hold. The annotationVec<HashSet<char>>tells the compiler, which is why the declarations carry it. charcells. A board cell is achar, written with single quotes, sodigit == '.'compares characters. The digits stay characters; nothing here needs their numeric value.- Indexing with
usize.randccome from0..9and are used to index vectors, so they areusize, and so isbox_index. ||short-circuits. If the row set already contains the digit, the column and box sets are not even consulted.- Early return. The first repeat ends the function, so a bad board is rejected as soon as the second copy is visited.
Complexity
Time: O(81), a constant. The loops visit 81 cells, and each visit does a few set lookups and inserts that take constant time on average. Because the board size is fixed, this is the same as O(1). The point is that each cell is looked at once rather than once for every cell sharing its row, column or box.
Space: O(81). The 27 sets hold at most 81 digits between them, since each filled cell is stored three times at most, once per kind of group.
Edge cases and common mistakes
- A repeat in one kind of group only. A board can repeat a digit in a box without repeating it in any row or column. With a 5 at row 0, column 0 and another 5 at row 2, column 2, the function returns
falsebecause of the box alone. A repeat in only a row, or only a column, is caught by the matching set in the same way. - Treating
'.'as a digit. Without thecontinue, the second empty cell in a row would look like a repeat. An empty board of 81 dots must returntrue. - Wrong box formula.
r / 3 + c / 3maps different boxes to the same number. The box row must be multiplied by 3. - Forgetting to insert. A check without the three
insertcalls never finds anything and always returnstrue. - Checking solvability. The function never tries to complete the board. The third example is valid even though it has no solution.
- Passing a value instead of a reference.
contains(digit)doesn't compile. The compiler reportsmismatched types,expected `&char`, found `char`, and suggests borrowing with&digit.
Testing
- The first example board returns
true. - The same board with an 8 in the top-left corner returns
false. - Row 0 set to
12345678.with a single 9 at row 4, column 8 returnstrue. - A board of 81 dots returns
true. - Two 5s at row 0, column 0 and row 2, column 2 return
false(same box). - Two 7s in row 4, at columns 0 and 8, return
false(same row). - Two 9s in column 4, at rows 0 and 8, return
false(same column).
