Start learning free

LESSON 291 OF 414 · BACKTRACKING

Sudoku Solver in Scala: Backtracking on a 9x9 Board

Practice O(nn2) Read the full explanation

Write a program to solve a Sudoku puzzle by filling the empty cells.

A sudoku solution must satisfy all of the following rules:

  1. Each of the digits 1-9 must occur exactly once in each row.
  2. Each of the digits 1-9 must occur exactly once in each column.
  3. Each of the digits 1-9 must occur exactly once in each of the 9 3x3 sub-boxes of the grid.

The '.' character indicates empty cells.

 

Example 1:



Input: board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
Output: [["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
Explanation: The input board is shown above and the only valid solution is shown below:


 

Constraints:

  • The board has 9 rows.
  • Each row has 9 cells.
  • Each cell holds a digit or '.'.
  • It is guaranteed that the input board has only one solution.

STEP 1 OF 31
    Ctrl + Enter to check

    SCALA · BACKTRACKING

    Sudoku in Scala, explained

    Problem overview

    The method solveSudoku receives a 9 by 9 board, an Array[Array[Char]], where a digit character '1' to '9' is a clue and '.' is an empty cell. It must fill every empty cell so that each row, each column and each of the nine 3 by 3 boxes contains the digits 1 to 9 exactly once, and return the solved board. The puzzle is guaranteed to have exactly one solution.

    The board uses Char values, written with single quotes: '5' and '.' are characters, while "5" would be a String.

    Approach: fill the empty cells in order, undo wrong guesses

    A human solves Sudoku by reasoning about which digit must go where. A program can instead search. Walk through the cells in reading order, left to right and top to bottom:

    1. If the cell already holds a clue, move on to the next cell.
    2. If it is empty, try the digits '1' to '9'. Whenever a digit breaks no rule, write it in and continue with the next cell.
    3. If the rest of the board can't be completed, the recursion returns, the digit is erased and the next candidate is tried.
    4. When the walk passes the last cell, the board is complete.

    This is backtracking: guess, recurse, undo. Rejecting a digit that conflicts with a row, column or box cuts the search down so that it finishes quickly on ordinary puzzles.

    The solved board has to be remembered somewhere, because the undo step erases the guesses while the recursion unwinds. The lesson stores a copy of the finished board in a field, result. A non-null result also tells every pending call to stop exploring.

    Step by step

    1. Add the field var result: Array[Array[Char]] = null to the class, and set this.result = null at the start of solveSudoku so an earlier call can't leak into this one.
    2. Start the search at the top-left cell with this.backtracking(0, 0, board), then return this.result.
    3. Declare backtracking(row, col, board) returning Unit.
    4. Stop when done. If this.result != null, a solution has been captured, so return. If row == 9, every cell has been passed, so call saveBoard(board) and return.
    5. Find the next cell once, with getNextCell, and keep its row and column as newRow and newCol.
    6. Skip clues. If board(row)(col) != '.', continue with the next cell and return.
    7. Try the digits with for (value <- '1' to '9'). The loop variable is a Char, and it is named value because val is a keyword.
    8. Guess: when isValid(board, row, col, value) holds, write board(row)(col) = value.
    9. Recurse into the next cell, then undo with board(row)(col) = '.'.
    10. Write saveBoard. It allocates new Array[Array[Char]](board.length), whose slots start as null, and fills each slot with board(i).clone(). Cloning each row makes a separate copy that later undo steps can't change.
    11. Write getNextCell. At column 8 it returns the first column of the next row, Array(row + 1, 0); otherwise Array(row, col + 1). From cell (8, 8) this gives row 9.
    12. Write isValid. Scan all 81 cells with two nested loops. A cell conflicts if it already holds value and shares a row, a column or a box with (row, col). The box test compares row / 3 with i / 3 and col / 3 with j / 3: integer division maps rows 0-2 to 0, 3-5 to 1 and 6-8 to 2. If any cell conflicts, return false; after the loops, return true.

    The solution in Scala

    class Solution {
        def isValid(board: Array[Array[Char]], row: Int, col: Int, value: Char): Boolean = {
            for (i <- 0 until 9) {
                for (j <- 0 until 9) {
                    val usedValue = board(i)(j) == value
                    val sameRowOrCol = row == i || col == j
                    val sameRowBox = row / 3 == i / 3
                    val sameColBox = col / 3 == j / 3
                    val sameBox = sameRowBox && sameColBox
                    if (usedValue && (sameRowOrCol || sameBox)) {
                        return false
                    }
                }
            }
            return true
        }
    
        def getNextCell(row: Int, col: Int): Array[Int] = {
            if (col == 8) {
                return Array(row + 1, 0)
            }
            return Array(row, col + 1)
        }
    
        def saveBoard(board: Array[Array[Char]]): Unit = {
            this.result = new Array[Array[Char]](board.length)
            for (i <- 0 until board.length) {
                this.result(i) = board(i).clone()
            }
        }
    
        def backtracking(row: Int, col: Int, board: Array[Array[Char]]): Unit = {
            if (this.result != null) {
                return
            }
            if (row == 9) {
                saveBoard(board)
                return
            }
            val newRow = getNextCell(row, col)(0)
            val newCol = getNextCell(row, col)(1)
            if (board(row)(col) != '.') {
                this.backtracking(newRow, newCol, board)
                return
            }
            for (value <- '1' to '9') {
                if (this.isValid(board, row, col, value)) {
                    board(row)(col) = value
                    this.backtracking(newRow, newCol, board)
                    board(row)(col) = '.'
                }
            }
        }
    
        var result: Array[Array[Char]] = null
        def solveSudoku(board: Array[Array[Char]]): Array[Array[Char]] = {
            this.result = null
            this.backtracking(0, 0, board)
            return this.result
        }
    }
    

    Scala details:

    • Fields and member order. var result is declared after the methods that use it. In a Scala class the order of members doesn't matter for methods and fields, so this is fine.
    • Character ranges. '1' to '9' is a range of Char values. The body receives one character at a time.
    • Updating a cell. board(row)(col) = value changes the board in place. The caller's array is modified during the search and restored by the undo steps.
    • Copies. Arrays are references. Without clone() in saveBoard, the result would share rows with the working board and the undo steps would erase it.
    • return inside for. isValid returns from within nested for loops. Scala 3.5 accepts it but warns that non-local returns are no longer supported (they work by throwing an exception that the method catches).

    Complexity

    The board has a fixed size, so the work is bounded by a constant, but that constant can be huge. In the worst case the search could try 9 digits in each of the 81 cells, and every isValid call scans 81 cells. Ordinary puzzles are solved quickly because most digits are rejected at once. The memory is small: the board, one saved copy and a recursion at most 81 cells deep.

    Edge cases and common mistakes

    • The input board is restored. After solveSudoku returns, the board you passed in is back to its original clues and dots, because the undo step runs while the recursion unwinds. The returned array is a separate copy. For the puzzle in the example, the first row of the result is 534678912.
    • No solution. If a puzzle can't be completed, result stays null, so the method returns null. For example, a first row 12345678. with a 9 lower in the last column leaves the corner cell with no valid digit, and the result is null.
    • A full board. If there are no dots, every cell is a clue and the walk goes straight to row 9, so the board is copied and returned.
    • Comparing the cell with itself. The target cell is empty ('.') while it is tested, so it never counts as a conflict.
    • Strings instead of characters. Assigning "." to a cell of an Array[Array[Char]] does not compile: a cell holds a Char, not a String.
    • Forgetting the undo. A wrong guess would stay on the board and block the correct digit for good.
    • Box mix-up. The box condition needs both row / 3 == i / 3 and col / 3 == j / 3. Testing only one of them marks a whole band of boxes as conflicting.

    Testing

    • The example puzzle, whose first row is 53..7...., comes back with the first row 534678912 and the last row 345286179.
    • isValid on the first row, column 2, accepts '4' and rejects '5' (same row), '8' (same box) and '6' (same box).
    • getNextCell(0, 8) is [1, 0], and getNextCell(8, 8) is [9, 0].
    • Solving a board that is already complete returns an identical board.
    Keep going with ScalaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free