Backtracking
A backtracking algorithm begins with an empty solution and extends the solution step by step. The search recursively goes through all different ways how a solution can be constructed.
Queen problem
As an example, consider the problem of calculating the number of ways $n$ queens can be placed on an $n \times n$ chessboard so that no two queens attack each other. For example, when $n=4$, there are two possible solutions:
The problem can be solved using backtracking by placing queens to the board row by row. More precisely, exactly one queen will be placed on each row so that no queen attacks any of the queens placed before. A solution has been found when all $n$ queens have been placed on the board.
For example, when $n=4$, some partial solutions generated by the backtracking algorithm are as follows:
At the bottom level, the three first configurations are illegal, because the queens attack each other. However, the fourth configuration is valid and it can be extended to a complete solution by placing two more queens to the board. There is only one way to place the two remaining queens.
The algorithm can be implemented as follows:
#![allow(unused)] fn main() { let n = 4; let mut count = 0; let mut column = vec![false;n]; let mut diag1 = vec![false;2*n-1]; let mut diag2 = vec![false;2*n-1]; search(0, n, &mut count, &mut column, &mut diag1, &mut diag2); println!{"{count}"}; fn search(y: usize, n: usize, count: &mut usize, column: &mut Vec<bool>, diag1: &mut Vec<bool>, diag2: &mut Vec<bool>) { if y == n { *count += 1; return; } for x in 0..n { if column[x] || diag1[x + y] || diag2[x.wrapping_sub(y).wrapping_add(n).wrapping_sub(1)] { continue; } column[x] = true; diag1[x + y] = true; diag2[x + n - y - 1] = true; search(y + 1, n, count, column, diag1, diag2); column[x] = false; diag1[x + y] = false; diag2[x + n - y - 1] = false; } } }
wrapping_sub
is used to avoid that a negative number has used asusize
The search begins by calling search(0, n, &mut count, &mut column, &mut diag1, &mut diag2)
;
The size of the board is $n \times n$,
and the code calculates the number of solutions
to count
.
The code assumes that the rows and columns
of the board are numbered from 0 to $n-1$.
When the function search
is
called with parameter $y$,
it places a queen on row $y$
and then calls itself with parameter $y+1$.
Then, if $y=n$, a solution has been found
and the variable count
is increased by one.
The Vec
column
keeps track of columns
that contain a queen,
and the arrays diag1
and diag2
keep track of diagonals.
It is not allowed to add another queen to a
column or diagonal that already contains a queen.
For example, the columns and diagonals of
the $4 \times 4$ board are numbered as follows:
Let $q(n)$ denote the number of ways to place $n$ queens on an $n \times n$ chessboard. The above backtracking algorithm tells us that, for example, $q(8)=92$. When $n$ increases, the search quickly becomes slow, because the number of solutions increases exponentially. For example, calculating $q(16)=14772512$ using the above algorithm already takes about a minute on a modern computer1
1 There is no known way to efficiently calculate larger values of $q(n)$. The current record is $q(27)=234907967154122528$, calculated in 2016 [55].