Problem overview
The method solveNQueens receives an integer n and returns every way to place n queens on an n by n chessboard so that no two queens attack each other. Queens attack along rows, columns and diagonals. Each solution is a sequence in which entry r is the column of the queen on row r. The solutions may come in any order, and the result type is mutable.ArrayBuffer[mutable.ArrayBuffer[Int]].
For n = 4 there are two solutions, [1, 3, 0, 2] and [2, 0, 3, 1]. In the first, the queens stand at (row 0, column 1), (row 1, column 3), (row 2, column 0) and (row 3, column 2).
Approach: place one queen per row, undo when stuck
With n queens on n rows and no two sharing a row, every row holds exactly one queen. So the only choice is which column to use in each row. The brute force tries all combinations of a column for each row, n to the power n of them, which is far too many. Backtracking builds the board row by row and abandons a partial board as soon as it contains an attack:
- Standing at
row, try each column from 0 to n - 1. - If a queen at (
row,col) is safe from the queens already placed above, place it and move to the next row. - When the recursion comes back, remove that queen and try the next column.
- When
row == n, all rows have a queen: record a copy of the board.
"Place, recurse, remove" is the shape of every backtracking solution. Because unsafe squares are rejected right away, whole subtrees of hopeless boards are never visited.
A queen at (row, col) is attacked by the queen in an earlier row i at column placement(i) if they share a column (placement(i) == col) or a diagonal. Two squares are on a diagonal when the row distance equals the column distance, so the test is Math.abs(row - i) == Math.abs(col - placement(i)). Rows can't clash, because only one queen goes in each.
Step by step
- Create the result collection in
solveNQueens:mutable.ArrayBuffer[mutable.ArrayBuffer[Int]](). Each inner buffer will be one finished board. - Create the working board,
mutable.ArrayBuffer[Int](). Its entryris the column of the queen in rowr, and it grows and shrinks as the search moves. - Start the search with
backtracking(n, 0, placement, placements), then returnplacements. - Declare
backtrackingwithn,row, the working board and the result collection, returningUnit. - Complete board: if
row == n, addplacement.clone()toplacementsand return. The copy matters because the same buffer keeps changing afterwards. - Loop over the columns with
for (col <- 0 until n). - Test the square with
isValid(placement, row, col). - Place: append with
placement += col. - Recurse:
backtracking(n, row + 1, placement, placements). - Undo:
placement.remove(placement.size - 1)drops the last entry. On anArrayBuffer,removetakes an index. - Write
isValid. Loopiover the earlier rows,0 until row. If the queen in rowishares the column or a diagonal, returnfalse. After the loop, returntrue.
The solution in Scala
class Solution {
def isValid(placement: mutable.ArrayBuffer[Int], row: Int, col: Int): Boolean = {
for (i <- 0 until row) {
if (placement(i) == col ||
Math.abs(row - i) == Math.abs(col - placement(i))) {
return false
}
}
return true
}
def backtracking(n: Int, row: Int,
placement: mutable.ArrayBuffer[Int],
placements: mutable.ArrayBuffer[mutable.ArrayBuffer[Int]]): Unit = {
if (row == n) {
placements += placement.clone()
return
}
for (col <- 0 until n) {
if (isValid(placement, row, col)) {
placement += col
backtracking(n, row + 1, placement, placements)
placement.remove(placement.size - 1)
}
}
}
def solveNQueens(n: Int): mutable.ArrayBuffer[mutable.ArrayBuffer[Int]] = {
val placements = mutable.ArrayBuffer[mutable.ArrayBuffer[Int]]()
val placement = mutable.ArrayBuffer[Int]()
backtracking(n, 0, placement, placements)
return placements
}
}
Scala details:
- The type
mutable.ArrayBuffer. It is a growable array, from the packagescala.collection.mutable. In a file of your own you needimport scala.collection.mutablefor this code to compile. +=adds, andremovetakes an index.placement += colappends one element.placement.remove(placement.size - 1)removes the element at that index, here the last one.clone(). A buffer is a reference. Addingplacementitself would store the same object many times, and by the end of the search it is empty, so every stored board would be empty. With a copy, each stored board keeps its contents. Without the copy, the result forn = 4is two empty buffers.returninsidefor. Scala 3 compiles aforover a range into a call with a function body, so thereturn falseinisValidworks by throwing an exception that the method catches. The compiler accepts it but warns that non-local returns are no longer supported and suggestsscala.util.boundary. Anexistscall over0 until row, or awhileloop, avoids the warning.0 until n. It excludesn, which is what a column index needs.
Complexity
Time. There is no tight closed form. At most n choices exist in the first row, at most n - 1 columns are free in the second, and so on, so n! bounds the number of boards the search can reach, and the early rejection cuts that down a great deal. Each safety check costs O(n). The number of solutions itself grows quickly: 1, 0, 0, 2, 10, 4, 40, 92 and 352 for n from 1 to 9.
Space. The working board holds up to n entries and the recursion is n calls deep, so the search uses O(n) memory besides the stored solutions, which take up to n numbers each.
Edge cases and common mistakes
- n = 1. The only board is one queen, so the result is
[[0]]. - n = 2 and n = 3. No placement works, and the result is an empty buffer. The search finishes without ever reaching
row == n. - Counts. For
n = 4ton = 9the result has 2, 10, 4, 40, 92 and 352 boards. - Order. Columns are tried from 0 upward, so the boards come out in increasing order. For
n = 4the first is[1, 3, 0, 2], and forn = 6it is[1, 3, 5, 0, 2, 4]. - Forgetting to undo. Without the
remove, leftover queens from a failed branch stay in the buffer and later checks compare against queens that are no longer there. - Storing the working buffer. Skipping
clone()corrupts the results, as above. - Checking only the column. A diagonal attack is a row difference equal to a column difference. For the partial board
[1, 3], column 2 on row 2 is rejected because the queen at row 1, column 3 is one row and one column away.
Testing
n = 1gives[[0]].n = 2andn = 3give no solutions.n = 4gives[[1, 3, 0, 2], [2, 0, 3, 1]].n = 6gives 4 solutions, andn = 8gives 92.
