Start learning free

LESSON 292 OF 415 · BACKTRACKING

Sudoku Solver in Swift: Backtracking Over [[Character]]

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

    SWIFT · BACKTRACKING

    Sudoku in Swift, explained

    Problem overview

    The method solveSudoku receives a 9 by 9 Sudoku board as [[Character]], where each cell is a digit from "1" to "9" or "." for 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. The puzzle is guaranteed to have exactly one solution, and the method returns the solved board.

    The statement's example starts with ["5","3",".",".","7",".",".",".","."] as its first row, and the completed first row is 5 3 4 6 7 8 9 1 2. Here is how the example board comes out, one string per row:

    534678912
    672195348
    198342567
    859761423
    426853791
    713924856
    961537284
    287419635
    345286179
    

    Approach: guess, recurse, undo

    The solver visits cells in reading order: left to right across a row, then the next row. At a cell that already has a digit, it simply moves on. At an empty cell, it tries the digits 1 to 9 in turn. A digit is allowed only if it doesn't already appear in the cell's row, column or box. When a digit is allowed, the solver writes it into the board and continues with the next cell. If that path eventually fails, control comes back, the solver erases the digit and tries the next one. This is backtracking: make a choice, explore the consequences, undo the choice.

    When the position moves past the last row, every cell has been filled legally, so the board is a solution. Because the puzzle has just one, the search can stop at that point. But "stop" needs care. The calls that are waiting on the stack would otherwise carry on trying other digits and undoing them, which would erase the solution from the board. The lesson's way out is a property called result. When a solution is found, a snapshot of the board is saved there. Every call checks result first and returns at once if a solution is saved, so the remaining calls unwind without doing any work.

    Step by step

    1. Add a property result of type [[Character]]?, starting as nil. It holds the solved board once one is found, and nil means "not solved yet". solveSudoku resets it to nil at the start.
    2. Start the search at the top-left cell: self.backtracking(0, 0, &board).
    3. Return self.result!. The ! unwraps the optional, which is safe because a solution has been saved.
    4. Declare backtracking with a row, a col and the board as an inout [[Character]].
    5. Stop conditions. If result is not nil, return. If row == 9, the board is complete, so call saveBoard(board) and return.
    6. Find the next cell with getNextCell and keep its row and column as newRow and newCol.
    7. Skip given digits. If board[row][col] is not ".", recurse on the next cell and return.
    8. Try the digits. Loop over the characters of "123456789". If isValid accepts the digit, write it into the cell, recurse on the next cell, then write "." back.
    9. Write saveBoard: set result to an empty array and append each row of the board to it.
    10. Write getNextCell: if the column is 8, the next cell is column 0 of the next row, otherwise the next column in the same row.
    11. Write isValid: loop over all 81 cells (i, j). A cell conflicts if it holds the candidate digit and lies in the same row or column as (row, col), or in the same box. Two cells share a box when row / 3 == i / 3 and col / 3 == j / 3. Return false on any conflict, true otherwise.

    The solution in Swift

    class Solution {
        func isValid(_ board: [[Character]], _ row: Int, _ col: Int, _ val: Character) -> Bool {
            for i in 0..<9 {
                for j in 0..<9 {
                    let usedValue = board[i][j] == val
                    let sameRowOrCol = row == i || col == j
                    let sameRowBox = row / 3 == i / 3
                    let sameColBox = col / 3 == j / 3
                    let sameBox = sameRowBox && sameColBox
                    if usedValue && (sameRowOrCol || sameBox) {
                        return false
                    }
                }
            }
            return true
        }
        func getNextCell(_ row: Int, _ col: Int) -> [Int] {
            if col == 8 {
                return [row + 1, 0]
            }
            return [row, col + 1]
        }
        func saveBoard(_ board: [[Character]]) {
            self.result = []
            for i in 0..<board.count {
                self.result!.append(board[i])
            }
        }
        func backtracking(_ row: Int, _ col: Int, _ board: inout [[Character]]) {
            if self.result != nil {
                return
            }
            if row == 9 {
                saveBoard(board)
                return
            }
            let newRow = getNextCell(row, col)[0]
            let newCol = getNextCell(row, col)[1]
            if board[row][col] != "." {
                self.backtracking(newRow, newCol, &board)
                return
            }
            for val in "123456789" {
                if self.isValid(board, row, col, val) {
                    board[row][col] = val
                    self.backtracking(newRow, newCol, &board)
                    board[row][col] = "."
                }
            }
        }
        var result: [[Character]]? = nil
        func solveSudoku(_ board: inout [[Character]]) -> [[Character]] {
            self.result = nil
            self.backtracking(0, 0, &board)
            return self.result!
        }
    }
    

    The Swift details worth a closer look:

    • Character, not String or Int. The board is [[Character]], so a literal such as "." is read as a Character where the context asks for one. Looping with for val in "123456789" goes over the characters of the string, which is why val can be written straight into the board and passed to isValid.
    • Optional result. A type followed by ? can hold nil as well as a value. self.result != nil tests for a saved solution, and self.result! unwraps it. If no solution had been saved, that ! would stop the program, but the guarantee of exactly one solution prevents that here. Writing self.result!.append(...) in saveBoard is safe because the line just before assigns [].
    • Value semantics make the snapshot work. Arrays copy when assigned, so the rows appended to result are independent copies. Later writes of "." to board don't alter the saved solution. After the call, board is back to the puzzle that was passed in, and the solution lives only in the returned value.
    • inout and &. board is passed as &board through the recursion so every call edits the same grid. The helpers isValid and saveBoard take it as an ordinary parameter, which only reads it.
    • Integer division. row / 3 truncates, so rows 0 to 2 map to 0, rows 3 to 5 to 1, and rows 6 to 8 to 2. Two cells are in the same box when both their row index and their column index map to the same value.
    • Properties can be declared after use. result is declared below the methods that use it, which is fine inside a class.

    Complexity

    Time: exponential in the worst case. With e empty cells there are up to 9^e digit sequences, and the search visits only the ones that survive the validity test. Real puzzles prune very hard, so they solve quickly. The test in isValid scans all 81 cells each time, a fixed cost per attempt, which is larger than the 27 cells it actually needs.

    Space: O(1) beyond the board. The recursion is at most 81 calls deep, and the saved snapshot holds 81 characters. These sizes don't depend on the puzzle, because the board is always 9 by 9.

    Edge cases and common mistakes

    • Already-filled cells. The solver moves on without testing them. The recursion has to return after that call, or the code would try to overwrite the clue with other digits.
    • Forgetting to undo. Without board[row][col] = ".", a failed guess stays on the board and blocks the correct digits later.
    • No early stop. Without the check on result, the search would finish the whole tree even after finding the answer. For a puzzle with one solution that wastes time, and only the snapshot saves the answer.
    • Saving too late. The undo steps erase the digits as the recursion unwinds, so the snapshot has to be taken when the position reaches row 9. Swift copies the arrays on assignment, which keeps the snapshot safe from those later writes.
    • Comparing digits as numbers. A Character such as "5" can't be compared with the integer 5. Keep both sides as Character.
    • Mixing up the box test. A cell is in the same box only if both the row block and the column block match. Testing just one of them marks whole bands of three rows or columns as conflicts.
    • A puzzle with no solution. The final self.result! would fail. The statement guarantees one solution.

    Testing

    • The example puzzle in the statement returns the nine strings shown at the top of this article, row by row.
    • After the call, the board that was passed in is back to the original puzzle, and the solution is in the return value.
    • A board that is already complete returns itself, because every cell is skipped and the position reaches row 9.
    Keep going with SwiftAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free