Problem overview
Place n queens on an n by n chessboard so that no two attack each other: no two share a row, a column or a diagonal. solveNQueens(n) returns all distinct solutions, in any order. Each solution is a list of n numbers, where entry r is the column of the queen in row r.
For n = 4 there are two solutions: [[1, 3, 0, 2], [2, 0, 3, 1]]. In the first one the queens are at row 0 column 1, row 1 column 3, row 2 column 0 and row 3 column 2. The lesson states 1 <= n <= 9.
Approach
Since each row must hold exactly one queen (two in a row would attack each other), we can build a solution one row at a time. For row 0 we try every column. For each choice that is safe, we move on to row 1 and try every column that doesn't clash with the queens above, and so on. If we reach row n, every row has a queen and we have found a solution. If a row has no safe column, the search backs up and changes an earlier choice. This is backtracking: add a choice, explore, undo the choice.
The key is the safety test. A queen at (row, col) attacks a queen at (i, c) if they share a column (c == col) or a diagonal. Two squares are on the same diagonal exactly when the row distance equals the column distance: |row - i| == |col - c|. We never need to test rows, because we place one queen per row.
Pruning on every row means the search never explores boards that already contain an attack, which is far fewer than all the n^n ways to pick one column per row.
Step by step
- Prepare the containers.
placementsis amutableListOf<MutableList<Int>>()that collects the finished boards.placementis amutableListOf<Int>()holding the columns chosen so far for rows 0, 1, 2, and so on. - Start the search with
backtracking(n, 0, placement, placements)and then returnplacements. - Declare
backtracking. Each call handles one row, withplacementalready holding the columns of the rows above it. - Solution found. If
row == n, add a copy ofplacementtoplacementsand return. The copy matters:placementkeeps changing as the search continues, and a stored reference would change with it. - Try each column. Loop
colover0 until n. - Check safety. Only when
isValid(placement, row, col)is true do we place a queen. - Choose, explore, undo. Add
coltoplacement, recurse withrow + 1, and then remove the last element withremoveAt(placement.size - 1). - Write
isValid. Loopiover0 until row. Rowihas its queen atplacement[i]. Returnfalsefor the same column or the same diagonal. If the loop finishes without a clash, returntrue.
The solution in Kotlin
class Solution {
fun isValid(placement: MutableList<Int>, row: Int, col: Int): Boolean {
for (i in 0 until row) {
if (placement[i] == col ||
Math.abs(row - i) == Math.abs(col - placement[i])) {
return false
}
}
return true
}
fun backtracking(n: Int, row: Int,
placement: MutableList<Int>,
placements: MutableList<MutableList<Int>>) {
if (row == n) {
placements.add(ArrayList(placement))
return
}
for (col in 0 until n) {
if (isValid(placement, row, col)) {
placement.add(col)
backtracking(n, row + 1, placement, placements)
placement.removeAt(placement.size - 1)
}
}
}
fun solveNQueens(n: Int): MutableList<MutableList<Int>> {
val placements = mutableListOf<MutableList<Int>>()
val placement = mutableListOf<Int>()
backtracking(n, 0, placement, placements)
return placements
}
}
Kotlin details:
removeAtversusremove. On aMutableList<Int>,removeAt(index)removes by position, whileremove(element)removes the first element equal to the value. With[10, 20, 30],removeAt(1)leaves[10, 30]and thenremove(30)leaves[10]. Here we want the last position, soremoveAt(placement.size - 1)is right.ArrayList(placement)creates a new list with the same elements. Without it, every stored solution would be the same object that the search later empties.0 until rowis a range that excludesrow. Whenrowis 0 the range is empty, so the first queen is always safe.- Mutable and read-only lists. The parameters are
MutableList, so the helper can add and remove elements. A plainListcouldn't be changed. - Splitting a condition. The line break after
||is fine: the operator at the end of the line tells Kotlin that the expression continues. Math.abscomes from Java and works onInt.kotlin.math.absis the Kotlin equivalent.
A trace for n = 4
Row 0 tries column 0. Row 1 can't use column 0 (same column) or column 1 (diagonal), so it takes column 2. Row 2 finds columns 0 and 2 blocked by columns, column 1 and column 3 by diagonals, and has no safe square. The search removes the queen from row 1 and tries column 3, and keeps going until the column-0 branch is exhausted. Row 0 then moves to column 1, which leads to [1, 3, 0, 2].
Complexity
Each call tries up to n columns, and isValid costs O(n). A rough bound for the whole search is O(n!) nodes, since the number of safe columns drops with each row, times O(n) per check. The exact count is lower because many branches are cut early. The memory is O(n) for the recursion and the current placement, plus the stored solutions, each of size n.
The number of solutions grows quickly: n = 1 to 9 give 1, 0, 0, 2, 10, 4, 40, 92 and 352 solutions.
Edge cases and common mistakes
- n = 1. One queen on a 1 by 1 board: the result is
[[0]]. - n = 2 and n = 3. No solution exists, so the result is an empty list.
- Not copying the placement. You would get a list of identical empty lists at the end, because the same object is emptied by the final undo steps.
- Using
removeinstead ofremoveAt. Removing by value deletes the first matching column, which may be a different queen. - Forgetting the undo. Later rows would see queens from branches that were already abandoned.
- Testing only rows and columns. The diagonal check is what makes two queens in
(0, 0)and(1, 1)invalid. - Looping to
rowinclusive. Rowrowhas no queen yet, soplacement[row]would throwIndexOutOfBoundsException.
Testing
solveNQueens(4)returns[[1, 3, 0, 2], [2, 0, 3, 1]].solveNQueens(6)returns[[1, 3, 5, 0, 2, 4], [2, 5, 1, 4, 0, 3], [3, 0, 4, 1, 5, 2], [4, 2, 0, 5, 3, 1]].solveNQueens(8)has 92 entries.solveNQueens(3)is empty.