While looking at applications of recursion, I stumbled across a video for solving sudokus using Python by Pr. Thorsten Altenkirch from the University of Nottingham (UK) . The elegant solution got me wondering what an idiomatic Rust implementation would look like. Here is what I came up with.
Modelling the Sudoku grid
Sudokus have a very straightforward code representation as they are 9x9 grids of numbers. Each cell can either contain an integer between 1 to 9 inclusively, or nothing at all. The empty cells are filled sequentially respecting simple unicity rules along rows, columns and 3x3 blocks. A sudoku is solved when no empty cells remain.
Rust being a statically typed language, the most relevant approach is to fill empty cells with the number 0. This way, a sudoku grid can be defined by a two-dimensional array of type usize. Here is the declaration and assignment of our example:
let mut sudoku : [[usize;9];9] = [
[0,2,8, 3,0,0, 0,0,4],
[5,0,6, 0,0,0, 0,0,0],
[7,0,0, 0,4,0, 0,0,0],
[0,9,3, 0,0,0, 0,0,0],
[8,0,0, 1,0,0, 6,0,0],
[0,0,0, 0,5,0, 0,0,8],
[0,0,0, 0,0,5, 0,4,6],
[1,0,0, 0,0,9, 0,0,3],
[0,5,0, 0,1,0, 9,2,0]
];
Variable scope and helper functions
In the Python implementation, the sudoku grid is set as a global variable. Doing so in Rust comes with an important limitation: Only closures have access to global variables, and those who do, can not be recursively called. Given that the solver relies on recursion, the appropriate course of action is rather to use functions that take the sudoku grid variable as a mutable reference should the function need to modify it.
The first function we can write is the helper function possible
that will check if a particular integer n
can be inserted in the sudoku grid at position (i,j)
. This function will check uniqueness of the value n
along the relevant row, column and 3x3 block. The most idiomatic way to approach this implementation in Rust is to make use of ranges and iterators. Here is the function’s code:
fn possible(grid: &[[usize;9];9], i: usize, j: usize, n: usize) -> bool {
let (i0, j0) = ( (i / 3) * 3, (j / 3) * 3 );
let block_check = (j0..j0+3).all(|index_j| (i0..i0+3).all(|index_i| grid[index_j][index_i] != n) );
let rowcol_check = (0..9).all(|k| (grid[j][k] != n) && (grid[k][i] != n));
rowcol_check && block_check
}
The all
method for iterators is used extensively to check adherence to the unicity rules of the puzzle. Remember that this method uses short-circuiting, meaning that it will return false
as soon as a false
result is observed. This conveniently saves us from potential useless computations and allows for a very succinct code avoiding for
loops. The sudoku grid variable is passed as an immutable reference because the function should not modify it in any way.
for convenience, we will also define a simple helper function that sequentially prints in the console each row of the grid. It is uncreatively called print_sudoku
:
fn print_sudoku(grid: &[[usize;9];9]) {
for row in grid.into_iter() {
println!("{:?}", row);
}
println!("---------------------------");
}
Recursive function call
Now onto the solve
function that will use recursion and the backtracking concept. The function returns a boolean signalling whether a solution has been found or not. It is then called recursively at each incremental change of the sudoku grid. It is possible to avoid for
loops using the for_each
method for iterators, but it felt overkill in this case. Here is the code:
fn solve(grid: &mut [[usize;9];9]) -> bool {
for j in 0..9 {
for i in 0..9 {
if grid[j][i] == 0 {
for n in 1..10 {
if possible( grid, i, j, n) {
// Passing a mutable reference as immutable is perfectly allowed
grid[j][i] = n; // If possible, assign value
if solve( grid ) { print_sudoku( grid ) } // Recursive call on the modified grid
grid[j][i] = 0; // If unsuccessful, reset value
}
}
return false; // No value found for this grid position, solve failed
}
}
}
true
}
The function as it is written will print all solutions it encounters consecutively, as the print_sudoku
function is called when the inner solve
function call returns true
. If no solutions are found, nothing gets printed. In our example, this is what the function calls generates:
solve(&mut sudoku);
// prints a unique solution:
// [9, 2, 8, 3, 6, 1, 5, 7, 4]
// [5, 4, 6, 9, 7, 2, 3, 8, 1]
// [7, 3, 1, 5, 4, 8, 2, 6, 9]
// [6, 9, 3, 2, 8, 7, 4, 1, 5]
// [8, 7, 5, 1, 9, 4, 6, 3, 2]
// [4, 1, 2, 6, 5, 3, 7, 9, 8]
// [2, 8, 9, 7, 3, 5, 1, 4, 6]
// [1, 6, 7, 4, 2, 9, 8, 5, 3]
// [3, 5, 4, 8, 1, 6, 9, 2, 7]
// ---------------------------
Feel free to put this algorithm to the test and even extend it to suit more elaborate sudoku grids. If you like this post, follow me on Twitter and get notified on the next posts.