Problem overview
The method SolveNQueens receives an integer n and must return every way to place n queens on an n by n chessboard so that no two queens attack each other. A queen attacks along its row, its column and both diagonals.
Each solution is a List<int> that holds the column of the queen in each row: entry 0 is the column in row 0, entry 1 is the column in row 1, and so on, with columns counted from 0. The method returns a List<List<int>> of all such solutions, in any order.
For n = 4 there are two solutions, [1, 3, 0, 2] and [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:
. Q . .
. . . Q
Q . . .
. . Q .
Approach: place one queen per row and undo bad guesses
A board with n queens and no two in the same row must have exactly one queen in each row. That removes one of the three attack rules and gives a natural plan: go row by row, and in each row try every column. If a queen at (row, col) is safe from the queens already placed above, place it and move on to the next row. When a row has no safe column, the recursion simply returns, and the previous row tries its next column. That is backtracking: choose, explore, then undo the choice.
Checking safety only needs the queens in earlier rows, since later rows are still empty. A new queen at (row, col) conflicts with an earlier queen at (i, placement[i]) in two cases:
- They share a column:
placement[i] == col. - They share a diagonal: the distance between the rows equals the distance between the columns, so
Math.Abs(row - i) == Math.Abs(col - placement[i]).
Two queens are never in the same row, because each row receives exactly one.
When row reaches n, all rows have a queen, and the current placement is a complete solution. It is copied into the result list.
Step by step
- Create the result list in
SolveNQueens: aList<List<int>>namedplacements. - Create the working list,
placement. It holds the columns chosen so far, one per row above the current one, and grows and shrinks as the recursion goes deeper and returns. - Start at row 0 with
Backtracking(n, 0, placement, placements). - Return
placementsafter the search. - Declare
Backtracking. Each call handles one row: it takesn, the currentrow, the working list and the result list. - Record complete boards. If
row == n, add a copy ofplacementtoplacementsand return. A copy is essential, because the working list keeps changing afterwards. - Try every column of the current row with a loop from 0 to
n - 1. - Test it with
IsValid(placement, row, col). - Choose:
placement.Add(col)puts the queen on the board. - Explore:
Backtracking(n, row + 1, ...)fills the next row. - Undo:
placement.RemoveAt(placement.Count - 1)removes the queen again so the next column starts from the same state. - Write
IsValid. Loop over the earlier rows0torow - 1, returnfalseon a column or diagonal clash, and returntrueif no clash is found.
The solution in C#
class Solution {
public bool IsValid(List<int> placement, int row, int col) {
for (int i = 0; i < row; i++) {
if (placement[i] == col ||
Math.Abs(row - i) == Math.Abs(col - placement[i])) {
return false;
}
}
return true;
}
public void Backtracking(int n, int row, List<int> placement, List<List<int>> placements) {
if (row == n) {
placements.Add(new List<int>(placement));
return;
}
for (int col = 0; col < n; col++) {
if (IsValid(placement, row, col)) {
placement.Add(col);
Backtracking(n, row + 1, placement, placements);
placement.RemoveAt(placement.Count - 1);
}
}
}
public List<List<int>> SolveNQueens(int n) {
List<List<int>> placements = new List<List<int>>();
List<int> placement = new List<int>();
Backtracking(n, 0, placement, placements);
return placements;
}
}
The C# details worth a closer look:
List<T>is a reference type. Addingplacementitself toplacementswould store the same object many times, and by the end all entries would show the same final (empty) list.new List<int>(placement)uses the copy constructor to take a snapshot.RemoveAtversusRemove.RemoveAt(index)deletes the element at a position, andplacement.Count - 1is the last one.Remove(value)deletes the first element equal to a value, which is not what backtracking needs. For aList<int>, it would also be easy to confuse the two, because an index and a value are bothint.- Counting and indexing. A
List<T>usesCountfor its size and an indexerplacement[i]for access, unlike an array'sLength. The indexer throws ifiis out of range;IsValidstays within range becauseplacementalways has exactlyrowentries when rowrowis being tried. Math.Abs. It takes the absolute value of anint, so the diagonal test doesn't care which queen is to the left.- The line break in the condition. A C# statement ends at the semicolon, so the
||at the end of the first line simply continues the expression onto the next line.
Complexity
Time: exponential. Without any checks there would be n to the power n ways to choose a column in every row. The column check alone brings that down to at most n! placements, and the diagonal check prunes far more, because a bad queen stops its whole subtree at once. Each IsValid call costs O(n). No simple closed form exists for how many nodes the search visits, but for the limit of 9 in this lesson, it finishes quickly.
Space: O(n) besides the output. The recursion is n calls deep and the working list holds at most n columns. The result itself holds one list of n numbers per solution.
Edge cases and common mistakes
n = 1has one solution,[0].n = 2andn = 3have no solutions, so the method returns an empty list, notnull.- Known counts. For
nfrom 4 to 9 the number of solutions is 2, 10, 4, 40, 92 and 352. These are handy to test against. - Forgetting the undo step. Without
RemoveAt, the working list keeps stale columns and every later row is checked against queens that are no longer there. - Storing the working list directly. As noted above, the result then contains copies of the same list reference.
- A one-sided diagonal test. Checking
row - i == col - placement[i]withoutMath.Absonly catches one diagonal direction. - Looping past
row - 1inIsValid. Rows fromrowdown are empty. Readingplacement[row]would throw anArgumentOutOfRangeException.
Testing
n = 4gives[1, 3, 0, 2]and[2, 0, 3, 1], in this order, because columns are tried from 0 upward.n = 1gives[0].n = 3gives an empty list.n = 8gives 92 solutions.