Start learning free

LESSON 266 OF 347 · BACKTRACKING

Sudoku Solver in C#: Backtracking on a char[,] 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:

  • 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

    C# · BACKTRACKING

    Sudoku in C#, explained

    Problem overview

    The method SolveSudoku receives a 9 by 9 board as a char[,]. Each cell holds a digit from '1' to '9' or '.' for an empty cell. The method must fill the empty cells 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, and return the solved board. The puzzle is guaranteed to have exactly one solution.

    For a typical puzzle that starts with the rows 53..7.... and 6..195..., the solved board begins 534678912 and 672195348. The digits that were given never change, and the dots turn into digits.

    Approach: fill cells in order, undo wrong guesses

    Solving by pure reasoning is hard to code, but a systematic search is simple. Walk through the cells in reading order: left to right along row 0, then row 1, and so on. At an empty cell, try the digits 1 to 9. A digit is allowed only if the same digit isn't already in that cell's row, column or 3 by 3 box. When a digit is allowed, write it into the board and continue with the next cell. If that path eventually gets stuck, because some later cell has no allowed digit, the recursion returns, the guess is erased, and the next digit is tried. That is backtracking.

    Cells that already hold a digit are skipped. When the walk moves past the last cell, which is the same as reaching row 9, every cell has been filled legally, and the board is a solution.

    One subtlety: the search undoes every guess while it unwinds, so by the time the recursion finishes, the board is back to its starting state. The solution is therefore copied into a separate array the moment it is found.

    Step by step

    1. Add a field for the answer. char[,] result stores the solved board. SolveSudoku sets it to null first, so that a previous call can't leave an old answer behind. A non-null value later means "already solved".
    2. Start the search at the top-left cell with Backtracking(0, 0, board).
    3. Return result once the search ends.
    4. Declare Backtracking. It takes a row, a column and the board and returns nothing; success is reported through result.
    5. Stop early. If result is already set, return immediately, so every pending call unwinds without trying more digits. If row == 9, the walk passed the last cell, so save the board with SaveBoard and return.
    6. Compute the next cell, into new_row and new_col, from GetNextCell, which returns a two-element array.
    7. Skip given digits. If the cell isn't '.', continue with the next cell and return.
    8. Try digits. A char loop runs from '1' to '9'.
    9. Check each digit with IsValid.
    10. Commit the digit by writing it into the board, recurse into the next cell, and then undo it by writing '.' back.
    11. Write SaveBoard, which allocates result with the board's dimensions and copies the cells row by row.
    12. Write GetNextCell. At column 8 it moves to column 0 of the next row, and otherwise it moves one column right.
    13. Write IsValid. Scan all 81 cells. A cell conflicts when it already holds the digit and shares a row, a column or a box with the target cell.

    The solution in C#

    class Solution {
        public bool IsValid(char[,] board, int row, int col, int val) {
            for (int i = 0; i < 9; i++) {
                for (int j = 0; j < 9; j++) {
                    var usedValue = board[i, j] == val;
                    var sameRowOrCol = row == i || col == j;
                    var sameRowBox = row / 3 == i / 3;
                    var sameColBox = col / 3 == j / 3;
                    var sameBox = sameRowBox && sameColBox;
                    if (usedValue && (sameRowOrCol || sameBox)) {
                        return false;
                    }
                }
            }
            return true;
        }
    
        public int[] GetNextCell(int row, int col) {
            if (col == 8) {
                return new int[]{row + 1, 0};
            }
            return new int[]{row, col + 1};
        }
    
        public void SaveBoard(char[,] board) {
            this.result = new char[board.GetLength(0), board.GetLength(1)];
            for (int i = 0; i < board.GetLength(0); i++) {
                Array.Copy(board, i * board.GetLength(1), this.result, i * board.GetLength(1), board.GetLength(1));
            }
        }
    
        public void Backtracking(int row, int col, char[,] board) {
            if (this.result != null) {
                return;
            }
            if (row == 9) {
                SaveBoard(board);
                return;
            }
            int new_row = GetNextCell(row, col)[0];
            int new_col = GetNextCell(row, col)[1];
            if (board[row, col] != '.') {
                this.Backtracking(new_row, new_col, board);
                return;
            }
            for (char val = '1'; val <= '9'; ++val) {
                if (this.IsValid(board, row, col, val)) {
                    board[row, col] = val;
                    this.Backtracking(new_row, new_col, board);
                    board[row, col] = '.';
                }
            }
        }
    
        char[,] result;
        public char[,] SolveSudoku(char[,] board) {
            this.result = null;
            this.Backtracking(0, 0, board);
            return this.result;
        }
    }
    

    The C# details worth a closer look:

    • Two-dimensional arrays. A char[,] is indexed board[row, col], with a comma. The jagged form board[row][col] belongs to char[][] and doesn't compile on a rectangular array. GetLength(0) is the number of rows and GetLength(1) the number of columns.
    • Characters are numbers. A char converts implicitly to int by its character code: '5' is 53, not 5. In IsValid, the char argument val is widened to an int, and board[i, j] == val compares the character code of the cell with it, so the comparison is correct. To get the digit's value, subtract: '5' - '0' is 5.
    • A char loop variable. for (char val = '1'; val <= '9'; ++val) works because char supports ++ and comparison, and the digits '1' to '9' have consecutive codes.
    • Array.Copy on 2D arrays. It treats a multidimensional array as one flat run of cells in row-major order, so row i starts at offset i * board.GetLength(1) in both arrays, and copying GetLength(1) cells copies exactly one row.
    • Allocation. new char[9, 9] creates all 81 cells at once, filled with the character code 0. It makes a new array: the caller's board and the returned result are different objects.
    • var. Each var in IsValid is inferred as bool from its right-hand side, and row / 3 uses integer division, which maps rows 0 to 2 to 0, rows 3 to 5 to 1 and rows 6 to 8 to 2.
    • Naming. The names new_row and new_col follow the lesson; idiomatic C# would write newRow and newCol for local variables.

    Complexity

    Time: exponential in the worst case. With E empty cells, there are up to 9 to the power E ways to fill them, and the rules cut most branches early. Each IsValid call costs a constant number of steps, 81 cell checks. For a standard 9 by 9 puzzle, the search is fast in practice, because most guesses fail within a few cells.

    Space: constant besides the answer. The recursion is at most 81 calls deep, the board is modified in place, and the saved result is one more 9 by 9 array.

    Edge cases and common mistakes

    • Forgetting the undo. Without board[row, col] = '.', a failed guess stays on the board and blocks the correct digit for other cells.
    • Returning the board instead of a copy. The undo step erases every guess while the recursion unwinds, so the board itself ends up as the original puzzle again. The copy in SaveBoard keeps the solution.
    • Comparing with a number. Checking a cell against 5 instead of '5' never matches, because the cell holds the character code 53.
    • Overwriting given digits. The != '.' check must come before the digit loop.
    • Not stopping after the first solution. The result != null check ends the search at once. Without it, the search would keep trying digits after a solution was found.
    • Box arithmetic. Two cells share a box only when both row / 3 == i / 3 and col / 3 == j / 3 hold. Testing only one of them would reject digits in the same band of three rows but a different box.
    • The target cell itself. It is empty during the check, so it can't count as a conflict.

    Testing

    • For the puzzle with first row 53..7...., the solved first row is 534678912 and the last row is 345286179.
    • After the call, the board you passed in holds the original puzzle again, and the returned array holds the solution.
    • A puzzle that is already complete comes back unchanged.
    Keep going with C#About 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free