A clear guide to solving N-Queens with backtracking, row-by-row placement, and constant-time conflict checks.
Problem Restatement
We are given an integer n.
We need to place n queens on an n x n chessboard so that no two queens attack each other.
A queen can attack another queen if they share:
- The same row
- The same column
- The same diagonal
Return all distinct valid board configurations. Each board uses "Q" for a queen and "." for an empty square. The answer may be returned in any order. The official constraint is 1 <= n <= 9.
Input and Output
| Item | Meaning |
|---|---|
| Input | An integer n |
| Output | A list of valid boards |
| Board format | Each board is a list of n strings |
| Cell format | "Q" means queen, "." means empty |
| Constraint | No two queens may attack each other |
Function shape:
def solveNQueens(n: int) -> list[list[str]]:
...Examples
For:
n = 4There are two valid boards:
[
[".Q..", "...Q", "Q...", "..Q."],
["..Q.", "Q...", "...Q", ".Q.."]
]One board means:
.Q..
...Q
Q...
..Q.The queens are placed at:
row 0, col 1
row 1, col 3
row 2, col 0
row 3, col 2No two queens share a row, column, or diagonal.
For:
n = 1The answer is:
[["Q"]]A single queen on a 1 x 1 board is valid.
First Thought: Brute Force
The most direct idea is to try all possible queen placements on the board.
The board has n * n cells.
We need to choose n cells and then check whether the placement is valid.
That approach wastes a lot of work because it tries many impossible boards.
For example, if two queens are already in the same row, the board can never become valid. We should reject that path immediately.
Key Insight
Since we need to place n queens on n rows, and no two queens can share a row, every valid board must have exactly one queen in each row.
So we can build the board row by row.
For each row:
- Try each column.
- Check whether placing a queen there is safe.
- If safe, place the queen and move to the next row.
- After exploring that path, remove the queen and try another column.
This is backtracking.
The important part is the safety check.
A queen at position (row, col) conflicts with an earlier queen if they share:
| Conflict | How to detect |
|---|---|
| Same column | col already used |
| Same main diagonal | row - col already used |
| Same anti-diagonal | row + col already used |
Why diagonals work:
Cells on the same main diagonal have the same row - col.
(0, 0), (1, 1), (2, 2)
row - col = 0Cells on the same anti-diagonal have the same row + col.
(0, 2), (1, 1), (2, 0)
row + col = 2Algorithm
Maintain three sets:
cols = set()
diag1 = set() # row - col
diag2 = set() # row + colAlso maintain a board:
board = [["."] * n for _ in range(n)]Then define a recursive function:
backtrack(row)Meaning:
Try to place queens from this row onward.
For each column in the current row:
- Check whether
colis already used. - Check whether
row - colis already used. - Check whether
row + colis already used. - If any check fails, skip this column.
- Otherwise, place the queen.
- Recurse to the next row.
- Remove the queen after recursion returns.
When row == n, we have placed all queens. Convert the board into a list of strings and add it to the answer.
Correctness
The algorithm places exactly one queen per row because each recursive call handles one row and places at most one queen before moving to the next row.
The sets prevent all possible attacks from earlier queens. cols prevents two queens from sharing a column. diag1 prevents two queens from sharing a main diagonal. diag2 prevents two queens from sharing an anti-diagonal.
Whenever the algorithm reaches row == n, it has placed n queens. Since every placement passed the column and diagonal checks, no two queens attack each other. Therefore every board added to the answer is valid.
The algorithm also finds every valid board. For each row, it tries every column. If a placement can belong to a valid board, the safety checks allow it. The recursion then continues to the next row. Since every possible safe row-by-row placement is explored, every valid solution is eventually added.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n!) approximately | One queen is placed per row, and used columns shrink the choices |
| Space | O(n^2) | The board stores n * n cells |
| Extra recursion space | O(n) | The recursion depth is n |
| Sets | O(n) | At most n columns and diagonals are active |
The output itself may contain many boards, so the total returned data can also be large.
Implementation
class Solution:
def solveNQueens(self, n: int) -> list[list[str]]:
ans = []
board = [["."] * n for _ in range(n)]
cols = set()
diag1 = set()
diag2 = set()
def backtrack(row: int) -> None:
if row == n:
ans.append(["".join(r) for r in board])
return
for col in range(n):
d1 = row - col
d2 = row + col
if col in cols or d1 in diag1 or d2 in diag2:
continue
board[row][col] = "Q"
cols.add(col)
diag1.add(d1)
diag2.add(d2)
backtrack(row + 1)
board[row][col] = "."
cols.remove(col)
diag1.remove(d1)
diag2.remove(d2)
backtrack(0)
return ansCode Explanation
We store all answers here:
ans = []The board starts empty:
board = [["."] * n for _ in range(n)]The three sets track attacked columns and diagonals:
cols = set()
diag1 = set()
diag2 = set()The recursive function receives the row we are currently filling:
def backtrack(row: int) -> None:If row == n, all rows already have queens:
if row == n:
ans.append(["".join(r) for r in board])
returnWe convert each row from a list of characters into a string because the required output format is list[str].
For each column:
for col in range(n):We compute the diagonal IDs:
d1 = row - col
d2 = row + colIf the column or diagonal is already attacked, this position cannot be used:
if col in cols or d1 in diag1 or d2 in diag2:
continueOtherwise, place the queen:
board[row][col] = "Q"
cols.add(col)
diag1.add(d1)
diag2.add(d2)Then solve the next row:
backtrack(row + 1)After that recursive call finishes, undo the move:
board[row][col] = "."
cols.remove(col)
diag1.remove(d1)
diag2.remove(d2)This undo step allows the algorithm to try the next column cleanly.
Testing
def normalize(ans):
return sorted(ans)
def run_tests():
s = Solution()
assert s.solveNQueens(1) == [["Q"]]
expected_4 = [
[".Q..", "...Q", "Q...", "..Q."],
["..Q.", "Q...", "...Q", ".Q.."],
]
assert normalize(s.solveNQueens(4)) == normalize(expected_4)
assert s.solveNQueens(2) == []
assert s.solveNQueens(3) == []
for board in s.solveNQueens(5):
n = len(board)
queens = []
for r in range(n):
for c in range(n):
if board[r][c] == "Q":
queens.append((r, c))
assert len(queens) == n
for i in range(len(queens)):
for j in range(i + 1, len(queens)):
r1, c1 = queens[i]
r2, c2 = queens[j]
assert r1 != r2
assert c1 != c2
assert abs(r1 - r2) != abs(c1 - c2)
print("all tests passed")
run_tests()| Test | Why |
|---|---|
n = 1 | Smallest valid board |
n = 4 | Standard example with two solutions |
n = 2 | No valid arrangement |
n = 3 | No valid arrangement |
n = 5 validation loop | Checks every returned board mechanically |