Start learning free

LESSON 267 OF 348 · BACKTRACKING

Sudoku Solver in Kotlin: Backtracking Over a Char Grid

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:

  • board.length == 9
  • board[i].length == 9
  • board[i][j] is a digit or '.'.
  • It is guaranteed that the input board has only one solution.

STEP 1 OF 28
    Ctrl + Enter to check

    KOTLIN · BACKTRACKING

    Sudoku in Kotlin, explained

    Problem overview

    Fill in a 9 by 9 Sudoku board so that every row, every column and every one of the nine 3 by 3 boxes contains each digit from 1 to 9 exactly once. The board is an Array<CharArray>: each cell is a Char, either a digit from '1' to '9' or '.' for an empty cell. solveSudoku(board) returns the completed board. The puzzle has exactly one solution.

    For a typical puzzle whose first row is 53..7...., the solved first row is 534678912.

    Approach

    A person solves Sudoku by logic. A program can simply try digits and take them back when they lead nowhere. That is backtracking:

    1. Go through the cells in reading order: left to right, then top to bottom.
    2. If the cell already has a digit, move on.
    3. If it is empty, try the digits '1' to '9'. For each digit that breaks no rule, write it in and continue with the next cell.
    4. If the continuation fails, erase the digit (put '.' back) and try the next one.
    5. If we walk past the last cell, every cell is filled legally, and the board is a solution.

    The same rules that define a solution also prune the search. A digit is rejected the moment it repeats in its row, column or box, so the program explores only boards that have no conflicts so far.

    Step by step

    1. A place for the answer. The class property var result: Array<CharArray>? = null will hold the solved board. solveSudoku sets it to null first, so a previous call can't leave an answer behind. Using a property lets the recursive helper report success without returning anything.
    2. Start at the top-left. Call this.backtracking(0, 0, board).
    3. Return the result. this.result!! converts the nullable property to a non-null value, and throws if it is still null.
    4. Stopping checks in backtracking. If result is already set, return at once, which unwinds all pending calls. If row == 9, the board is complete: call saveBoard(board) and return.
    5. Find the next cell with getNextCell, which returns a two-element array, row first and column second.
    6. Skip clues. If board[row][col] != '.', recurse on the next cell and return.
    7. Try digits. For an empty cell, loop over the character range '1'..'9'. The variable is called value, not val, because val is a Kotlin keyword.
    8. Choose, explore, undo. If isValid(board, row, col, value) holds, set the cell, recurse on the next cell and then set the cell back to '.'.
    9. Write saveBoard. It builds a new array with Array(board.size) { CharArray(0) } and then replaces each row with board[i].copyOf(). A snapshot is needed because the board keeps being edited while the recursion unwinds.
    10. Write getNextCell. In column 8 the next cell is column 0 of the next row. Otherwise it is the next column of the same row. From the last cell the answer is row 9, which the helper treats as done.
    11. Write isValid. Scan all 81 cells. A conflict exists when a cell already holds the digit and shares a row, a column or a box with the target. Two cells share a box when row / 3 == i / 3 and col / 3 == j / 3, because integer division maps rows 0 to 2 to 0, rows 3 to 5 to 1 and rows 6 to 8 to 2.

    The solution in Kotlin

    class Solution {
        fun isValid(board: Array<CharArray>, row: Int, col: Int, value: Char): Boolean {
            for (i in 0 until 9) {
                for (j in 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
        }
    
        fun getNextCell(row: Int, col: Int): IntArray {
            if (col == 8) {
                return intArrayOf(row + 1, 0)
            }
            return intArrayOf(row, col + 1)
        }
    
        fun saveBoard(board: Array<CharArray>) {
            this.result = Array(board.size) { CharArray(0) }
            for (i in board.indices) {
                this.result!![i] = board[i].copyOf()
            }
        }
    
        fun backtracking(row: Int, col: Int, board: Array<CharArray>) {
            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 in '1'..'9') {
                if (this.isValid(board, row, col, value)) {
                    board[row][col] = value
                    this.backtracking(newRow, newCol, board)
                    board[row][col] = '.'
                }
            }
        }
    
        var result: Array<CharArray>? = null
        fun solveSudoku(board: Array<CharArray>): Array<CharArray> {
            this.result = null
            this.backtracking(0, 0, board)
            return this.result!!
        }
    }
    

    Kotlin details:

    • Chars, not strings. A cell is a Char written with single quotes: '.' and '5'. Comparing board[i][j] == value compares two characters. A Char is not an Int, so '5' and 5 are different things.
    • A Char range. '1'..'9' is a range of characters, so for (value in '1'..'9') visits '1', '2' and so on up to '9'.
    • Nullable property. The type Array<CharArray>? allows null. After the check this.result != null Kotlin still doesn't smart-cast a mutable property, which is why saveBoard writes through this.result!!. The !! operator throws NullPointerException if the value is null.
    • copyOf() on a CharArray returns a new array with the same contents. Without copying the rows, the saved board would share them with the working board, and the undo steps would erase the answer.
    • The board is restored. After the solved board is saved, the recursion unwinds, and every board[row][col] = '.' line runs. When solveSudoku returns, the board you passed in is back to the original puzzle, and the answer is only in the returned array.
    • Property placement. result is declared below the methods that use it, which Kotlin allows inside a class.

    Complexity

    Worst-case backtracking over 81 cells is exponential, since each empty cell may try up to 9 digits. The rule checks prune the vast majority of branches, so a normal 9 by 9 puzzle finishes quickly. Each isValid call scans all 81 cells, a constant for this fixed size, so each step costs O(1). The recursion is at most 81 levels deep, and the extra memory is a constant: one copy of the board.

    Edge cases and common mistakes

    • Forgetting the undo. Without board[row][col] = '.', failed guesses stay on the board and poison later attempts.
    • Not copying the solution. After the unwinding, the working board is back to the puzzle, so a saved reference to it would show the original clues.
    • Not stopping after a solution. The early result != null check ends the search. Without it, the loops would continue trying digits and the code would keep doing pointless work.
    • A puzzle with no solution. The lesson guarantees one solution. If there were none, result would stay null and !! would throw NullPointerException. A first row such as 12345678. with a 9 at the bottom of the last column triggers it.
    • Box arithmetic. Compare row / 3 with i / 3 and col / 3 with j / 3. Comparing row % 3 would match unrelated cells.
    • Treating digits as numbers. Writing board[row][col] = 5 doesn't compile. The cell needs a Char, such as '5'.

    Testing

    • A board whose first row is 53..7.... and whose last row is ....8..79 returns a board with first row 534678912 and last row 345286179.
    • isValid(board, 0, 2, '5') on the original puzzle is false, because the first row already has a 5.
    • isValid(board, 0, 2, '4') on the same puzzle is true.
    Keep going with KotlinAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free