Sudoku Solver, a hard leetcode problem (backtracking)

Problem is here: https://leetcode.com/problems/sudoku-solver/description/

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 the digits 1-9 must occur exactly once in each of the 9 3x3 sub-boxes of the grid.
Empty cells are indicated by the character '.'.

A sudoku puzzle...

...and its solution numbers marked in red.
Note:
  • The given board contain only digits 1-9 and the character '.'.
  • You may assume that the given Sudoku puzzle will have a single unique solution.
  • The given board size is always 9x9.
The solution is via backtracking with pruning. Basically it is important to understand that there is only one solution, hence the moment that you find it you should cease all further processing.
The idea is to have 3 sets of hash tables keeping track of which numbers have already been used for each row (9 hash tables), column (9 hash tables) and quadrants (9 hash tables).
The backtracking will be controlled by an index from 0..81, and with this index you should be able to access the row, the column and the quadrant. The formula for the quadrant based on the index took me the longest to figure out, but with some paper/pen/trial/error, in few minutes you can find it (actual sketch below):


First add all the current numbers to the appropriate hash tables. Then start the backtracking: if you reach the end (index == 81), you have found the solution. If the current cell contains an initial number, skip it. Otherwise, backtrack it for all numbers 1..9. Code is below, cheers, Marcelo.


public class Solution
{
 public void SolveSudoku(char[][] board)
 {
  Hashtable[] rows = new Hashtable[9];
  Hashtable[] cols = new Hashtable[9];
  Hashtable[] quadrants = new Hashtable[9];

  for (int i = 0; i < 9; i++)
  {
   rows[i] = new Hashtable();
   cols[i] = new Hashtable();
   quadrants[i] = new Hashtable();
  }

  //Add all current values to the hash tables
  int index = 0;
  for (int r = 0; r < 9; r++)
  {
   for (int c = 0; c < 9; c++)
   {
    if (board[r][c] != '.')
    {
     AddNumber(index, (int)(board[r][c] - '0'), rows, cols, quadrants);
    }
    index++;
   }
  }

  SolveSudoku(board, 0, rows, cols, quadrants);
 }

 private bool SolveSudoku(char[][] board, 
        int index,
        Hashtable[] rows,
        Hashtable[] cols,
        Hashtable[] quadrants)
 {
  if (index == 81) return true;

  int r = index / 9;
  int c = index % 9;
  if (board[r][c] != '.') return SolveSudoku(board, index + 1, rows, cols, quadrants);
  else
  {
   for (int n = 1; n <= 9; n++)
   {
    if (AddNumber(index, n, rows, cols, quadrants))
    {
     board[r][c] = (char)(n + '0');
     if (SolveSudoku(board, index + 1, rows, cols, quadrants)) return true;
     board[r][c] = '.';
     RemoveNumber(index, n, rows, cols, quadrants);
    }
   }
  }

  return false;
 }

 private bool AddNumber(int index,
       int number,
       Hashtable[] rows,
       Hashtable[] cols,
       Hashtable[] quadrants)
 {
  int r = index / 9;
  int c = index % 9;
  int q = (index / 27) * 3 + (index % 9) / 3;
  if (!rows[r].ContainsKey(number) && !cols[c].ContainsKey(number) && !quadrants[q].ContainsKey(number))
  {
   rows[r].Add(number, true);
   cols[c].Add(number, true);
   quadrants[q].Add(number, true);
   return true;
  }
  return false;
 }

 private void RemoveNumber(int index,
        int number,
        Hashtable[] rows,
        Hashtable[] cols,
        Hashtable[] quadrants)
 {
  int r = index / 9;
  int c = index % 9;
  int q = (index / 27) * 3 + (index % 9) / 3;
  rows[r].Remove(number);
  cols[c].Remove(number);
  quadrants[q].Remove(number);
 }
}

Comments

  1. Thank you for sharing.

    I really like the ideas in your implementation. I enumerate one by one in the following:

    1. Preprocess all rows and columns and quadrants into data structure, O(1) to look up if the digit is available for next empty slot;
    2. index variable from 0 to 80 is used to track the progress of depth first search, help to identify base case;
    3. Step 1 is also very efficient since every element with digit values in the board will be added once at the beginning.

    I make some minor changes and add some comment to help follow the calculation.

    Hashtable -> HashSet
    variable names:
    rows -> rowDigits
    cols -> colDigits

    I rewrote the code and practice one more time, also I shared on Leetcode discuss.

    https://leetcode.com/problems/sudoku-solver/discuss/217272/C-One-more-elegant-solution

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. One more comment, I took some time to figure out the calculation and add some comment to help people to read the code of function AddNumber(...)
    ///
    /// The row and column should not include same digit more than once; quadrant as well.
    ///
    private static bool AddNumber(int index,
    int number,
    HashSet[] rows,
    HashSet[] cols,
    HashSet[] quadrants)
    {
    // one row has 9 columns
    int r = index / 9;
    int c = index % 9;

    // there are three rows and three columns quadrants.
    // each row will have 27 elements
    // each quadrant will have 9 elements
    // First row quadrant
    // Quadrant1 | Quadrant2 | Quadrant3
    // by index
    // 0 - 8 | 9 - 17 | 18 - 26
    // by column
    // 0 1 2 | 3 4 5 | 6 7 8

    int q = (index / 27) * 3 + (index % 9) / 3;

    if (!rows[r].Contains(number) &&
    !cols[c].Contains(number) &&
    !quadrants[q].Contains(number))
    {
    rows[r].Add(number);
    cols[c].Add(number);

    quadrants[q].Add(number);

    return true;
    }

    return false;
    }

    ReplyDelete
  4. Very nice!!! This problem reminds me of the 8-queens problem doesn’t it? Same ideas. Cheers, Marcelo

    ReplyDelete
  5. Loved this problem! There are actually a lot of tricks that can be used to prune the search space to speed up the solution space like considering the most constrained positions first, but since I'm lazy I went ahead with a brute-force in Python:

    class Solution:
    VALID_VALUES = set(map(str, range(1, 10)))

    def solveSudoku(self, board):
    """
    :type board: List[List[str]]
    :rtype: void Do not return anything, modify board in-place instead.
    """
    def rows(row):
    return board[row]

    def columns(column):
    for row in range(len(board)):
    yield board[row][column]

    def squares(row, column):
    start_row = (row // 3) * 3
    start_column = (column // 3) * 3
    for row in range(start_row, start_row+3):
    for column in range(start_column, start_column+3):
    yield board[row][column]

    def is_set(value):
    return value != '.'

    def safe(row, column):
    return self.VALID_VALUES - (set(rows(row)) | set(columns(column)) | set(squares(row, column)))

    def unset():
    for row in range(len(board)):
    for column in range(len(board[0])):
    if not is_set(board[row][column]):
    yield row, column

    def solve(pos_idx, positions):
    row, column = positions[pos_idx]
    for safe_value in safe(row, column):
    board[row][column] = safe_value
    if pos_idx == len(positions) - 1:
    return True
    solution = solve(pos_idx+1, positions)
    if solution:
    return True
    board[row][column] = '.'

    unset = list(unset())
    if not unset:
    return # already solved

    solve(0, unset)

    https://gist.github.com/ttsugriy/1f93921fa8fb46783d4d144f6c4b873b

    Thanks for sharing!

    ReplyDelete

Post a Comment

Popular posts from this blog

Advent of Code - Day 6, 2024: BFS and FSM

Golang vs. C#: performance characteristics (simple case study)

Advent of Code - Day 7, 2024: Backtracking and Eval