N-Queens
The N-Queens problem asks: place
nchess queens on ann × nboard so that no two queens attack each other (no two share a row, column, or diagonal). The original “eight queens” puzzle was posed by Max Bezzel in 1848 and has 92 solutions (12 unique up to symmetry). For generaln, the count grows superexponentially — sequence OEIS A000170 — and exact counts are known up ton = 27(computed in 2016 with massive parallelism). The problem is the canonical exemplar of backtracking with constraint propagation: assign one queen per row, prune any column already attacked, and recurse. With three boolean arrays tracking used columns, anti-diagonals (r + c), and main diagonals (r − c), each constraint check isO(1). A bitmask encoding makes the inner loop one word-AND instruction, and on modern hardware then = 14problem solves in milliseconds. The problem appears as LeetCode 51 (return all configurations) and LeetCode 52 (just count) and is the introductory case study for backtracking in CLRS, Knuth TAOCP Vol. 4A §7.2.2, and Russell-Norvig Ch. 6.
1. Intuition — One Queen Per Row, Skip Bad Columns
A queen attacks along its row, column, and both diagonals. So if we want n non-attacking queens on an n × n board:
- They must be in
ndifferent rows. (If two were in the same row, they’d attack along that row.) So we may as well assign one queen to each row — rowrgets the queen with indexr. - The remaining decision is: in row
r, which column does the queen go in? - The constraints are: that column hasn’t been used in any earlier row, AND no earlier queen is on the same diagonal.
Two diagonals matter: the anti-diagonal (top-left to bottom-right, where row + col is constant) and the main diagonal (top-right to bottom-left, where row − col is constant). If two queens have the same row + col, they share an anti-diagonal; if they have the same row − col, they share a main diagonal.
So: try each column c in row r. If c is unused and the diagonals r + c and r − c are both unused, place the queen and recurse to row r + 1. If we reach row n without conflicts, we have a valid arrangement — record it. On the way back up, undo the placement so the next iteration starts fresh.
The “constraint encoding” — three boolean arrays for columns / anti-diagonals / main-diagonals — is what makes this fast. Without it, every placement would scan up to O(n) to verify no attack — turning the algorithm from O(n!)-ish into O(n · n!)-ish per placement, much slower in practice.
2. Tiny Worked Example — n = 4
For n = 4, the only two solutions are:
. Q . . . . Q .
. . . Q Q . . .
Q . . . . . . Q
. . Q . . Q . .
(columns [1, 3, 0, 2] and [2, 0, 3, 1])
Let’s trace the backtracker on n = 4:
Row 0:
try col=0: cols={0}, diag1={0+0=0}, diag2={0-0+3=3}
Row 1:
try col=0: cols∋0, skip.
try col=1: diag1={1+1=2}? not in {0}; diag2={1-1+3=3}? IN {3}, skip (attacks diagonal).
try col=2: diag1={1+2=3}? not in {0}; diag2={1-2+3=2}? not in {3}. Place. cols={0,2}, diag1={0,3}, diag2={3,2}
Row 2:
try col=0: cols∋0, skip.
try col=1: diag1={2+1=3}? IN {0,3}, skip.
try col=2: cols∋2, skip.
try col=3: diag1={2+3=5}? not in; diag2={2-3+3=2}? IN {3,2}, skip.
all 4 cols fail → backtrack.
Undo: cols={0}, diag1={0}, diag2={3}.
try col=3: diag1={1+3=4}; diag2={1-3+3=1}. Place. cols={0,3}, diag1={0,4}, diag2={3,1}.
Row 2:
try col=0: cols∋0.
try col=1: diag1={2+1=3}? not in; diag2={2-1+3=4}? not in. Place. cols={0,3,1}, diag1={0,4,3}, diag2={3,1,4}.
Row 3:
try col=0: cols∋0.
try col=1: cols∋1.
try col=2: diag1={3+2=5}? not in; diag2={3-2+3=4}? IN {3,1,4}, skip.
try col=3: cols∋3.
all fail → backtrack.
Undo.
try col=2: diag1={2+2=4}? IN {0,4}, skip.
try col=3: cols∋3.
backtrack. Undo.
Undo (col=3).
Undo (col=0).
try col=1: cols={1}, diag1={1}, diag2={2}.
Row 1:
try col=3: diag1={1+3=4}; diag2={1-3+3=1}. Place. cols={1,3}, diag1={1,4}, diag2={2,1}.
Row 2:
try col=0: diag1={2+0=2}? not in; diag2={2-0+3=5}? not in. Place. cols={1,3,0}, diag1={1,4,2}, diag2={2,1,5}.
Row 3:
try col=2: diag1={3+2=5}? not in; diag2={3-2+3=4}? not in. Place. cols={1,3,0,2}.
✓ row 4 reached → record [1,3,0,2]
Undo.
Undo. (no more cols)
Undo.
... etc.
Without expanding further: solution 1 is found via (0,1), (1,3), (2,0), (3,2). The trace shows pruning at work — most attempted (row, col) pairs fail constraint checks immediately and the search tree is exponentially smaller than 4⁴ = 256. The algorithm visits only a few dozen nodes before finding both solutions.
3. Pseudocode
3.1 Set-Based Constraint Encoding
solve_n_queens(n):
results := []
queens := [] # queens[r] = column of queen in row r
cols := empty set
diag1 := empty set # anti-diagonal: r + c constant
diag2 := empty set # main diagonal: r − c constant
define backtrack(r):
if r == n:
results.append(render(queens, n))
return
for c := 0 to n − 1:
if c in cols or (r + c) in diag1 or (r − c) in diag2:
continue # PRUNE: attacks an existing queen
cols.add(c); diag1.add(r + c); diag2.add(r − c)
queens.append(c)
backtrack(r + 1)
queens.pop()
cols.remove(c); diag1.remove(r + c); diag2.remove(r − c)
backtrack(0)
return results
The five Backtracking Framework primitives map exactly:
| Primitive | This problem |
|---|---|
is_solution(state) | r == n (filled all rows) |
choices(state) | c in 0..n-1 (each column for the current row) |
valid(choice) | c not in cols and r+c not in diag1 and r-c not in diag2 |
apply(choice) | add c to cols, r+c to diag1, r-c to diag2; push c onto queens |
undo(choice) | exact mirror of apply |
The three sets together give O(1) constraint check (hash-set lookup) and O(1) apply/undo.
3.2 Bitmask Encoding (Faster)
For n ≤ 32 (or 64), the three sets fit in single integers and the constraint check is one bitwise-AND.
solve_n_queens_bitmask(n):
results := []
full := (1 << n) − 1 # all-bits mask
define backtrack(r, queens, cols_mask, diag1_mask, diag2_mask):
if r == n:
results.append(render(queens, n))
return
# All squares attacked by existing queens in this row:
attacked := cols_mask | diag1_mask | diag2_mask
free := full & ~attacked # bit set for each free column
while free != 0:
c_bit := free & −free # lowest set bit
c := bit_index(c_bit)
free ^= c_bit # mark as tried
backtrack(
r + 1, queens + [c],
cols_mask | c_bit,
(diag1_mask | c_bit) << 1, # diag1 shifts left by 1 per row
(diag2_mask | c_bit) >> 1) # diag2 shifts right by 1 per row
backtrack(0, [], 0, 0, 0)
return results
The diagonal trick is elegant: when we descend from row r to r + 1, every previously-set anti-diagonal “moves” one column to the right (on the next row). So we can encode diagonals positionally on the current row and just shift the mask by 1 each level. No need to track absolute r + c indices.
This is the bit-trick that makes counting solutions for n = 14 (365,596 solutions) take milliseconds.
4. Python Implementation
4.1 Set-Based Version (Clearest)
from typing import List
def solve_n_queens(n: int) -> List[List[str]]:
"""
Return all distinct solutions to the n-queens puzzle.
Each solution is a list of strings, where 'Q' marks a queen and '.' marks empty.
"""
results: list[list[str]] = []
queens: list[int] = [] # queens[r] = column of queen in row r
cols: set[int] = set()
diag1: set[int] = set() # r + c (anti-diagonal)
diag2: set[int] = set() # r - c (main diagonal)
def backtrack(r: int) -> None:
if r == n:
results.append(_render(queens, n))
return
for c in range(n):
if c in cols or (r + c) in diag1 or (r - c) in diag2:
continue # prune: attacks an existing queen
# apply
cols.add(c)
diag1.add(r + c)
diag2.add(r - c)
queens.append(c)
# recurse
backtrack(r + 1)
# undo (mirror of apply)
queens.pop()
cols.remove(c)
diag1.remove(r + c)
diag2.remove(r - c)
backtrack(0)
return results
def _render(queens: list[int], n: int) -> list[str]:
"""Convert a list of column indices to the string-grid format LC expects."""
rows = []
for c in queens:
rows.append('.' * c + 'Q' + '.' * (n - c - 1))
return rows
def count_n_queens(n: int) -> int:
"""LeetCode 52: just count solutions, don't render them."""
count = 0
cols: set[int] = set()
diag1: set[int] = set()
diag2: set[int] = set()
def backtrack(r: int) -> None:
nonlocal count
if r == n:
count += 1
return
for c in range(n):
if c in cols or (r + c) in diag1 or (r - c) in diag2:
continue
cols.add(c); diag1.add(r + c); diag2.add(r - c)
backtrack(r + 1)
cols.remove(c); diag1.remove(r + c); diag2.remove(r - c)
backtrack(0)
return count4.2 Bitmask Version (Fastest)
def count_n_queens_fast(n: int) -> int:
"""
Count solutions using bitmask encoding.
Roughly 5-10x faster than the set-based version for n ≥ 10.
"""
full = (1 << n) - 1
def go(cols: int, d1: int, d2: int) -> int:
if cols == full:
return 1
free = full & ~(cols | d1 | d2)
total = 0
while free:
bit = free & -free # lowest set bit
free ^= bit # remove from free
total += go(
cols | bit,
(d1 | bit) << 1 & full, # shift left, keep within board
(d2 | bit) >> 1)
return total
return go(0, 0, 0)The bitmask version is ~5-10× faster than the set version for n ≥ 10. For n = 14 (365,596 solutions) the bitmask version finishes in ~50 ms in pure Python; the set version takes ~500 ms.
5. Complexity
5.1 Worst Case
The unpruned search tree has n^n nodes (each row has n choices independently). With the “different column” prune alone, it shrinks to n! nodes (one queen per row, columns form a permutation). With diagonal pruning, the empirically observed tree is much smaller — for n = 8 only 2,057 nodes are visited (compare to 8! = 40,320 permutations, a ~20× pruning factor).
| n | # solutions | Empirical search-tree nodes (set version) |
|---|---|---|
| 4 | 2 | 17 |
| 5 | 10 | 41 |
| 6 | 4 | 119 |
| 7 | 40 | 469 |
| 8 | 92 | 2,057 |
| 9 | 352 | 9,945 |
| 10 | 724 | 53,973 |
| 12 | 14,200 | ~2,000,000 |
| 14 | 365,596 | ~46,000,000 |
(Counts of solutions from OEIS A000170; empirical node counts are approximate and depend on the implementation.)
5.2 Worst-Case Bound
The provable worst case is O(n!) time (the permutation bound), since with diagonal pruning the algorithm cannot exceed enumerating all n! row-column assignments. Per-node work is O(1) for set / bitmask operations, plus O(n) to render at the leaves. So total is O(n · n!) for the version that returns full configurations, O(n!) for the count-only version.
No tight closed-form for the pruned tree size
The “actually visited” tree size grows roughly as
Θ(c^n)for somec < n, but no closed-form is known. Knuth (TAOCP Vol. 4A §7.2.2) discusses this as the canonical example of a backtracking algorithm whose runtime is “estimable but not provable in tight form.” Empirically, the number of valid configurations grows as roughlyn! / c₁^nfor some constantc₁ ≈ 2.5, but this is a conjecture not a theorem.
5.3 Space
Recursion depth is exactly n. The three sets (or bitmasks) collectively store at most n elements each. So auxiliary space is O(n) (excluding the output). The output for “return all configurations” is O(n²) per configuration × number of configurations.
6. Bitmask Optimisation — Why It Works
Encoding the three “is this column / diagonal attacked” sets as integers (bitmasks) gives several wins:
O(1)constraint check is now one CPU instruction.cols | diag1 | diag2followed by& fulland~. The whole “is column c attacked?” question is(attacked >> c) & 1. Single-cycle on modern CPUs.- Diagonal shift trick. When the algorithm descends from row
rto rowr + 1, the “anti-diagonal” attack pattern moves one column to the right (because(r+1) + c = (r + (c-1)) + 1 + 1 — wait, that's not quite right. The actual identity: a queen at(r, c)blocks anti-diagonals at columnc + (Δr)for future rowsr + Δr. So when we increment the row, we should shift the diag1 mask left by one column. The(diag1 | c_bit) << 1does this in one instruction. Symmetrically fordiag2 >> 1. - Iterating free columns:
bit = free & -freeextracts the lowest set bit. This is the Bit Manipulation Tricks for “find lowest set bit” — single cycle on modern CPUs (BLSIinstruction). Thenfree ^= bitclears it. The iteration touches each free column exactly once, nofor c in range(n)scan.
For n ≤ 64, the masks fit in a single uint64. For larger n, you’d need bignum or block-based representation. In practice, all interview-relevant n (≤ 30) fit easily.
7. Symmetry-Reduction Optimisation
The chessboard has 8-fold symmetry (4 rotations × 2 reflections). Of the 92 solutions for n = 8, only 12 are unique up to symmetry. So you can cut the search space by ~8× by fixing the first queen to a specific quarter of the board and expanding by symmetry afterwards.
The simplest symmetry break: in row 0, only place the queen in columns 0 .. ⌊n/2⌋. After finding all such solutions, mirror them. (This requires care: solutions that are self-symmetric under reflection should not be doubled.)
For competitive performance benchmarks (e.g., the famous “n = 27” record set in 2016 using GPU symmetry-reduced backtracking), this is a 4-8× speedup.
8. Comparison with O(n) Construction Heuristics
For finding any one solution (not all), there are explicit construction methods:
- For
n ≥ 4andn mod 6 ∉ {2, 3}: queens at columns(2, 4, 6, ..., n, 1, 3, 5, ..., n-1)(even-then-odd) form a valid arrangement. - For
n mod 6 = 2: a more elaborate pattern. - For
n mod 6 = 3: another pattern.
These give O(n) solutions for “find one” but tell you nothing about the count or about all solutions — backtracking remains the algorithm of choice for those.
9. Diagram — The Constraint Encoding
flowchart TD Q["Queen at (r=2, c=3)"] Q --> ROW["Attacks row 2: same r"] Q --> COL["Attacks col 3: cols.add(3)"] Q --> D1["Attacks anti-diagonal: r+c=5: diag1.add(5)"] Q --> D2["Attacks main diagonal: r-c=-1: diag2.add(-1)"] ROW -.->|"unused: we place 1 queen per row"| INFO["row constraint is implicit"] Q2["Future queen at (r=4, c=6) — does it conflict?"] Q2 --> CHECK1["Same col? 6 in cols={3}? No"] Q2 --> CHECK2["Same anti-diag? 4+6=10 in diag1={5}? No"] Q2 --> CHECK3["Same main-diag? 4-6=-2 in diag2={-1}? No"] CHECK1 --> OK["all clear → place"] CHECK2 --> OK CHECK3 --> OK style Q fill:#fed style Q2 fill:#dfe style OK fill:#cfc
What this diagram shows. The top node is a queen placed at row 2, column 3. It attacks: its column (3), its anti-diagonal (where r + c = 5 is constant), and its main diagonal (where r − c = −1 is constant). The row attack is implicit because the algorithm places exactly one queen per row. The bottom subgraph illustrates how a future queen placement at (4, 6) is checked: three constant-time set-membership tests, one per attack direction. If all three checks pass, the queen can be placed and the constraint sets are extended. If any one fails, the algorithm immediately moves to the next column. This is the constraint-encoding insight that turns N-Queens from Θ(n^n) brute force into a tractable backtracking algorithm.
10. Pitfalls
- Forgetting to undo all three constraint sets. The most common bug. If
applymodifiescols,diag1,diag2, thenundomust reverse all three. Forgetting any one corrupts later branches and produces missing or extra “solutions”. Test onn = 4(only 2 solutions) — anything else means the undo is broken. - Using
r + cfor both diagonals. It’sr + cfor one andr − cfor the other. Reversing them or using only one (e.g.,r + cfor both) silently fails to detect attacks along one diagonal direction, producing many false positives. Easy to debug:n = 4should give exactly 2 solutions, not more. - Off-by-one in diagonal indices. If you use a fixed-size array instead of a hashset, indices are
r + c ∈ [0, 2n−2](size2n−1) andr − c ∈ [−(n−1), n−1]. Common shift:r − c + (n − 1)to get a non-negative index. Forgetting the shift indexes negative, which Python handles by wrapping (set/list[-1]) — silently wrong. - Recursing on
r + 1but checkingr >= ninstead ofr == n. Equivalent for the base case but inconsistent — oncer > n, you’ve gone too far.r == nis the canonical termination. - Mutating
queensinstead of snapshotting at the leaf.results.append(queens)(without[:]) appends the reference to the shared list. By recursion’s end,queensis empty and everyresultaliases it. Use_render(queens, n)(which builds a new list) orqueens[:]. - Using
list.remove(c)instead ofdiscard/ index-based removal.list.remove(c)isO(n)and finds the first matching value — for a list, this is fine but wasteful. With aset,remove(c)isO(1)(hash). Confusion between data structures sneaks in. - Not handling
n = 0orn = 1correctly.n = 0should return[[]](one trivial solution: no queens).n = 1should return[["Q"]].n = 2andn = 3should return[](no solutions). - Counting solutions but recursing into
solve_n_queensand discarding the output. That works but allocates a giantresultslist. Forn ≥ 13, allocate-as-you-go for the count-only version (LC 52) is much faster. - Bitmask version: forgetting
& fullafter the left-shift.(diag1 | bit) << 1can grow beyondnbits. The& fullmask keeps the diagonal pattern within the board. Without it, “fake” attacks creep in from off-board positions and cause wrong answers forn ≥ 4. - Treating it as “row-by-row” but using
rfrom a closure. If you use a recursive helper withras parameter (correct), you get clean recursion. If you storerin a closure variable and increment manually, the increment-then-undo dance must mirror the apply. Just use the parameter. - Believing
O(n!)is achievable in the worst case. It’s the bound; the actual visited tree is much smaller for moderatenthanks to pruning. But for any polynomial-time algorithm to exist would imply P = NP for the related “find a satisfying assignment” decision problem (which is harder); see Knuth’s discussion in TAOCP 4A.
11. Common Interview Problems
| Problem | LeetCode | Difficulty | Notes |
|---|---|---|---|
| N-Queens (return all configs) | 51 | Hard | The classic; this note’s main subject |
| N-Queens II (count only) | 52 | Hard | Same algorithm; skip the rendering. Bitmask shines here |
| Place K Knights (variant) | — | Medium | Knights’ attack pattern is more local; same backtracking schema |
| Place K Bishops Non-Attacking | — | Medium | Bishops only attack diagonals; can be split into “white squares” and “black squares” subproblems |
| Sudoku Solver | 37 | Hard | Sibling backtracking; row/col/box constraints. See Sudoku Solver |
| Word Search | 79 | Medium | Different state-space (grid path) but same backtracking schema. See Word Search |
| Combinations | 77 | Medium | Backtracking-101. See Combinations |
| Permutations | 46 | Medium | Backtracking-101. See Permutations |
12. Open Questions
- What’s the asymptotic growth rate of the number of solutions
Q(n)? OEIS A000170 conjecturesQ(n) ~ n! / c^nfor somec ≈ e^{1.5}, but no proven closed form. Bell-Shanks 2009 give the best current upper bound. - Q27, the count for
n = 27, was computed in 2016 via massively parallel backtracking on FPGAs. Q28 remains uncomputed as of 2026. What’s the practical limit with current hardware?c^ngrowth means eachΔn = 1isc ≈ 4.5×more work. - Constraint propagation: if you maintain “remaining legal columns per future row” and propagate when a queen is placed, can you prune more than just the diagonal/column check? AC-3 arc-consistency in CSP literature can shrink the tree but adds overhead. Practical win unclear for N-Queens specifically.
- How does N-Queens generalise to
mqueens on ann × nboard? The “canmqueens be placed?” decision problem is NP-complete (Gent et al. 2017), so the search-time scaling is fundamentally exponential.
13. See Also
- Backtracking Framework — the meta-skeleton; N-Queens is one of its four canonical instances
- Sudoku Solver — sibling problem with row/column/box constraints (3 constraint sets, like N-Queens)
- Word Search — sibling backtracking problem on a grid
- Generate Parentheses — sibling backtracking problem with simpler constraints
- Permutations — N-Queens columns form a permutation of
0..n-1; the diagonal constraint is what makes this not just “permute” - Bit Manipulation Tricks — the bitmask optimisation
- Depth-First Search — backtracking is DFS over the implicit state-space tree
- Big-O Notation
- SWE Interview Preparation MOC