Start learning free

LESSON 322 OF 573 · HASH TABLE

Valid Sudoku in Scala: One Pass with Mutable Sets

Practice O(81) Read the full explanation

A Sudoku board is a 9 x 9 grid of cells. Each cell holds a digit from '1' to '9', or '.' while it is empty. Rows 0-2, 3-5 and 6-8 and columns 0-2, 3-5 and 6-8 split the board into nine 3 x 3 boxes.

Given a partly filled board, return true if it is valid so far, and false otherwise. The board is valid when no digit appears twice:

  1. in the same row,
  2. in the same column,
  3. in the same 3 x 3 box.

Only the filled cells are checked; empty cells don't count. A valid board doesn't have to be solvable: you check the digits already on it, you don't solve the puzzle.

Rows are numbered from 0 at the top and columns from 0 at the left. In the examples, each line is one row of the board.

 

Example 1:



Input: board =
53..7....
6..195...
.98....6.
8...6...3
4..8.3..1
7...2...6
.6....28.
...419..5
....8..79
Output: true
Explanation: No row, column or box holds the same digit twice.

Example 2:

Input: board =
83..7....
6..195...
.98....6.
8...6...3
4..8.3..1
7...2...6
.6....28.
...419..5
....8..79
Output: false
Explanation: The board of Example 1 with an 8 instead of the 5 in the top left corner.
Column 0 now holds two 8s, in rows 0 and 3, and so does the top left box, in rows 0 and 2.

Example 3:

Input: board =
12345678.
.........
.........
.........
........9
.........
.........
.........
.........
Output: true
Explanation: No digit repeats, so the board is valid, although it has no solution:
the last cell of row 0 can only be a 9, and column 8 already has a 9 in row 4.

 

Constraints:

  • The board has 9 rows, and each row has 9 cells.
  • Each cell holds a digit from '1' to '9' or '.'.

Note:

Your algorithm should look at each cell once and check it in constant time: O(81) time and O(81) extra space for the 81 cells. Scanning the row, the column and the box of every cell again would also work on a board this small, but it looks at each cell many times.

STEP 1 OF 11
    Ctrl + Enter to check

    SCALA · HASH TABLE

    Valid Sudoku in Scala, explained

    Problem overview

    The method isValidSudoku receives a 9 x 9 board as an Array[Array[Char]]. Each cell holds a digit from '1' to '9', or '.' when it is empty. It returns true when no digit appears twice in the same row, the same column or the same 3 x 3 box. Only the filled cells are checked. The board doesn't have to be solvable: the task is to check the digits already placed, not to solve the puzzle.

    The classic partly filled board, starting with the rows 53..7.... and 6..195..., is valid. Changing its top-left 5 to an 8 makes column 0 hold two 8s, and the top-left box holds two as well, so the answer becomes false. A board whose first row is 12345678. with a 9 alone in row 4, column 8 is valid, even though the last cell of row 0 could never be filled: no digit repeats yet.

    Approach: remember what each row, column and box has seen

    The direct way is to scan the row, the column and the box of every filled cell again and look for a twin. On a 9 x 9 board that is fast enough, but it looks at each cell many times over.

    A cleaner way visits each cell once and keeps a record. There are 9 rows, 9 columns and 9 boxes, so keep 27 sets of digits: one per row, one per column, one per box. When a filled cell is reached, ask the three sets that cell belongs to whether they already contain its digit. If any does, the digit is a repeat and the board is invalid. If none does, add the digit to all three sets and move on. Every check is a set lookup, which takes constant time, so the whole board takes 81 cell visits.

    The only tricky part is naming the 9 boxes. Number them left to right, then top to bottom. A cell at row r and column c is in box row r / 3 and box column c / 3, using integer division, which drops the remainder. Its box number is (r / 3) * 3 + c / 3. Cell (4, 7) gives 1 * 3 + 2 = 5, the right box of the middle band. Cell (0, 0) is in box 0 and cell (8, 8) in box 8.

    Step by step

    1. Create the row sets. Array.fill(9)(scala.collection.mutable.Set[Char]()) builds an array of 9 empty mutable sets. rows(r) holds the digits seen so far in row r.
    2. Create cols and boxes the same way, so every cell has three sets to check.
    3. Loop over the rows with for (r <- 0 until 9).
    4. Loop over the columns inside it with for (c <- 0 until 9), so the cells are visited row by row.
    5. Read the cell into a val named digit. Indexing a nested array uses two pairs of parentheses: board(r)(c).
    6. Skip empty cells. Only when digit != '.' does the rest run. Scala has no continue, so the work sits inside the if.
    7. Compute the box number with the formula above.
    8. Look for a repeat. If rows(r), cols(c) or boxes(box) already contains the digit, return false at once.
    9. Record the digit in the row set, the column set and the box set, so later cells in the same row, column or box can find it.
    10. Finish with true. If every filled cell was checked against the earlier ones and nothing repeated, the board is valid. The last expression is the method's value.

    The solution in Scala

    class Solution {
        def isValidSudoku(board: Array[Array[Char]]): Boolean = {
            val rows = Array.fill(9)(scala.collection.mutable.Set[Char]())
            val cols = Array.fill(9)(scala.collection.mutable.Set[Char]())
            val boxes = Array.fill(9)(scala.collection.mutable.Set[Char]())
            for (r <- 0 until 9) {
                for (c <- 0 until 9) {
                    val digit = board(r)(c)
                    if (digit != '.') {
                        val box = (r / 3) * 3 + c / 3
                        if (rows(r).contains(digit) || cols(c).contains(digit) || boxes(box).contains(digit)) {
                            return false
                        }
                        rows(r).add(digit)
                        cols(c).add(digit)
                        boxes(box).add(digit)
                    }
                }
            }
            true
        }
    }
    

    The Scala details worth a closer look:

    • Mutable versus immutable sets. A plain Set in Scala is immutable: it has no way to add an element in place. scala.collection.mutable.Set can grow, which is what a record of seen digits needs. In a file of your own you could write import scala.collection.mutable and then mutable.Set[Char]().
    • Array.fill runs its second argument once per place. Because the set is created inside the second parameter list, each of the 9 slots gets its own set. Building one set first and filling the array with it would make all 9 slots share that set: adding a digit to one would show up in all the others.
    • val arrays can still change. rows is a val, so it can't be pointed at another array, but its elements are sets that change freely.
    • add returns a Boolean. It gives true for a new element and false for one already present. The solution ignores that value and uses contains to check first, which makes the three checks easy to read.
    • return inside a for. The loop body is a closure, and Scala 3.5 reports a warning, "Non local returns are no longer supported", for a return there. The program still compiles and returns the right answer. The recommended replacement is scala.util.boundary with boundary.break, or a while loop.
    • Characters compare directly. digit != '.' compares a Char with a Char. The digits stay characters throughout; they are never converted to numbers, because they are only compared for equality.

    Complexity

    Time: O(81), that is, constant. The loops visit 81 cells, and each visit does at most three set lookups and three insertions, each of which takes constant time on average.

    Space: O(81), also constant. The 27 sets together hold at most 81 digits, one per filled cell in each of its three sets, so at most 3 * 81 entries.

    Edge cases and common mistakes

    • An empty board. A board of nothing but '.' is valid. Empty cells never count, however many there are.
    • A repeat found by only one set. A 5 at the first cell of row 0 and another 5 at index 4 of the same row share a row but nothing else. Two 5s at (0, 0) and (8, 0) share only a column. Two 5s at (0, 0) and (1, 1) share only a box. Each of these boards returns false, which is why all three sets must be checked.
    • Treating '.' as a digit. Without the digit != '.' test, the second empty cell in a row would look like a repeat and every board would be invalid.
    • A wrong box formula. Writing r / 3 + c / 3 gives the same number to different boxes: boxes 1 and 3 would both be 1. Multiplying the box row by 3 is what makes the 9 numbers distinct.
    • Sharing one set across the array. Creating a set once and passing it to Array.fill makes every row share it, so the second row would reject any digit that appeared in the first.
    • Solving the puzzle. A valid board can have no solution. The method only checks the digits already placed.

    Testing

    • The classic board (53..7.... down to ....8..79) gives true.
    • The same board with an 8 instead of the top-left 5 gives false.
    • A board with 12345678. as its first row and a single 9 at row 4, column 8 gives true.
    • A board of only '.' gives true.
    • Two 5s in the same row, at columns 0 and 4, give false.
    • Two 5s in the same column, at rows 0 and 8, give false.
    • Two 5s at (0, 0) and (1, 1), in the same box only, give false.
    Keep going with Scala500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free