Non-Attacking Queens
Non-Attacking Queens
Category
Recursion
Difficulty
Very Hard
Problem Statement
Given a positive integer n, find the number of ways to place n queens on an n x n chessboard such that no two queens attack each other. Two queens attack each other if they share the same row, column, or diagonal. This is the classic N-Queens counting problem.
Intuition
Since each row must contain exactly one queen (otherwise two queens would share a row), we can build solutions row by row. For each row, we try placing a queen in each column and check if it conflicts with previously placed queens. The constraints to check are: no two queens in the same column, no two queens on the same diagonal (row - col), and no two queens on the same anti-diagonal (row + col). By using sets to track occupied columns and diagonals, each placement check is O(1).
Approach
- Process one row at a time, starting from row 0.
- Maintain three sets:
columns(occupied columns),diagonals(occupied row - col values), andantiDiagonals(occupied row + col values). - For each row, try placing a queen in each column 0 to n-1:
- If the column, diagonal, and anti-diagonal are all free, place the queen.
- Add the column, diagonal, and anti-diagonal to their respective sets.
- Recurse to the next row.
- Backtrack: remove the column, diagonal, and anti-diagonal from their sets.
- When row equals n, a valid arrangement has been found; increment the count.
- Return the total count.
Pseudocode
function nonAttackingQueens(n):
return solve(0, n, set(), set(), set())
function solve(row, n, columns, diagonals, antiDiagonals):
if row == n:
return 1
count = 0
for col from 0 to n - 1:
diag = row - col
antiDiag = row + col
if col in columns or diag in diagonals or antiDiag in antiDiagonals:
continue
columns.add(col)
diagonals.add(diag)
antiDiagonals.add(antiDiag)
count += solve(row + 1, n, columns, diagonals, antiDiagonals)
columns.remove(col)
diagonals.remove(diag)
antiDiagonals.remove(antiDiag)
return count
Optimized with bitmask:
function nonAttackingQueens(n):
return solve(0, n, 0, 0, 0)
function solve(row, n, colMask, diagMask, antiDiagMask):
if row == n:
return 1
count = 0
availableMask = ((1 << n) - 1) & ~(colMask | diagMask | antiDiagMask)
while availableMask > 0:
bit = availableMask & (-availableMask) # lowest set bit
availableMask -= bit
count += solve(row + 1, n,
colMask | bit,
(diagMask | bit) << 1,
(antiDiagMask | bit) >> 1)
return count
Time & Space Complexity
- Time: O(n!) in the upper bound. The first row has n choices, the second has at most n-1, and so on. The actual number of valid placements explored is much less due to diagonal constraints pruning branches early. No polynomial-time algorithm is known.
- Space: O(n) for the recursion stack and the constraint sets/bitmasks.
Key Insights
- Row-by-row placement inherently prevents row conflicts, reducing the problem to checking columns and diagonals only.
- The diagonal identity
row - colis constant along every top-left to bottom-right diagonal;row + colis constant along every top-right to bottom-left diagonal. - Bitmask representation replaces hash set lookups with bitwise operations, providing a significant constant-factor speedup.
- The problem exhibits no polynomial structure; backtracking with pruning is the standard approach.
- Known solutions for small n: n=1:1, n=2:0, n=3:0, n=4:2, n=5:10, n=6:4, n=7:40, n=8:92.
- Symmetry optimizations (reflecting/rotating the board) can cut the search space roughly by a factor of 8, but the implementation becomes more complex.
Edge Cases
- n = 1: exactly 1 solution (the single queen on the single cell).
- n = 2 or n = 3: no valid arrangement exists, return 0.
- n = 0: typically return 1 (the empty board is a valid arrangement with 0 queens).
- Large n (e.g., n > 15): runtime grows very fast; bitmask optimization and symmetry breaking become important for practical computation.