Algorithms

The N-Queens Problem: Backtracking, Bitmasks, and Why Brute Force Never Stood a Chance

An 8×8 chessboard has 4,426,165,368 ways to place 8 queens (that's C(64,8)). Exactly 92 of them leave no queen attacking another — and only 12 if you fold away rotations and reflections. Checking all 4.4 billion is absurd; a good backtracking solver finds all 92 by examining roughly 2,000 partial boards, pruning entire subtrees the instant a placement conflicts. That gap — billions versus thousands — is the whole point of backtracking.

The N-Queens problem, posed by chess composer Max Bezzel in 1848 and generalized by Franz Nauck in 1850, is the canonical teaching vehicle for systematic search with pruning. It has no known polynomial-time counting algorithm; the exact count for n = 27 was only settled in 2016 after a distributed computation. Yet a single valid arrangement can be constructed in O(n) with a closed-form trick.

  • Time (worst)O(N!)
  • SpaceO(N) recursion + O(N) state
  • InvariantNo two queens share row, column, or diagonal
  • Best forConstraint search with strong pruning
  • PosedBezzel 1848; Nauck (n) 1850
  • 8-queens solutions92 (12 fundamental)

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

The Core Idea and the Invariant

A queen attacks along its row, column, and both diagonals. The task: place N queens on an N×N board so that no two attack each other. The first structural insight collapses the search space: since no two queens may share a row, every solution places exactly one queen per row. So a candidate solution is a function from rows to columns — an array pos[0..N-1] where pos[r] is the column of the queen in row r. Because no two share a column either, that array must be a permutation of 0..N-1.

That reduces the space from C(N², N) to N! permutations — for N = 8, from 4.4 billion down to 40,320. The remaining constraint is diagonals. Two queens at (r₁, c₁) and (r₂, c₂) share a diagonal iff r₁ + c₁ = r₂ + c₂ (anti-diagonal, ╱) or r₁ − c₁ = r₂ − c₂ (main diagonal, ╲). So the loop invariant maintained at every step of the search is:

  • All placed queens occupy distinct columns,
  • distinct values of r + c (distinct ╱ diagonals), and
  • distinct values of r − c (distinct ╲ diagonals).

Backtracking builds a partial permutation row by row, extending only into columns that preserve this invariant, and abandons a branch the moment no legal column exists.

How Backtracking Works, Step by Step

The algorithm is a depth-first traversal of a search tree whose nodes are partial placements. At depth r we decide the column for the queen in row r:

  • Try each column c from 0 to N−1 for row r.
  • Check whether c is free of column, ╱, and ╲ conflicts against all rows placed so far.
  • Place it (mark the column and both diagonals occupied) and recurse to row r+1.
  • If the recursion returns without a full solution, undo the marks (backtrack) and try the next column.
  • Base case: when r == N, all rows are placed — record a solution.

The key optimization is O(1) conflict checking using three boolean sets instead of scanning prior rows. Because r + c ∈ [0, 2N−2] and r − c ∈ [−(N−1), N−1], we index the anti-diagonal by r + c and the main diagonal by r − c + N − 1 to keep it non-negative:

solve(r, cols, diag1, diag2, pos):
  if r == N: record(pos); return
  for c in 0..N-1:
    d1 = r + c
    d2 = r - c + N - 1
    if c in cols or d1 in diag1 or d2 in diag2:
      continue                 # prune
    cols.add(c); diag1.add(d1); diag2.add(d2); pos[r]=c
    solve(r+1, cols, diag1, diag2, pos)
    cols.remove(c); diag1.remove(d1); diag2.remove(d2)  # undo

The prune line is where the billions vanish: if placing the first three queens blocks every column in row 3, the entire subtree beneath that partial board — which could be N! / (something) leaves — is never generated.

Complexity Analysis: Where O(N!) Comes From

Time. Ignore pruning for the upper bound. Row 0 has N column choices, row 1 has at most N−1 remaining columns (column-distinctness), row 2 at most N−2, and so on — giving O(N!) leaf explorations. Each leaf/node does O(1) work with the set-based conflict check (or O(N) with naive scanning, yielding a looser O(N · N!)). So the worst-case bound is O(N!), and this is essentially tight for enumerating all solutions because the number of nodes visited is Θ of the permutation tree that survives column pruning.

Pruning changes the constant, not the class. Diagonal pruning removes a large fraction of the N! permutation tree — empirically the branching factor is far below N — but no polynomial bound is known, and none is expected: counting N-Queens solutions is #P-hard-adjacent in difficulty, and the exact count is unknown beyond n = 27. The practical effect is dramatic: for N = 8 the pruned tree has ≈ 2,057 nodes versus 40,320 permutation leaves; for finding just the first solution, the cost is empirically near-linear for the tractable range.

  • Space: O(N) for recursion depth plus O(N) for the three occupancy sets and pos array → Θ(N) total. Recording all solutions costs O(N · S) where S is the solution count.
  • Solution count growth: 8→92, 10→724, 12→14,200, 15→2,279,184 — super-exponential, so enumerating everything is inherently expensive regardless of algorithm.

The Bitmask Trick: Making It Fly

The set operations can be replaced by bitwise arithmetic on machine integers, an optimization popularized by Rivin, Vardi, and Zimmermann and by competitive-programming folklore. Represent columns, ╱ diagonals, and ╲ diagonals each as a single integer whose set bits mark occupied lines. At each row, the available columns are computed in a few instructions:

solve(cols, d1, d2):        # bits set = occupied
  if cols == FULL: count++; return
  free = FULL & ~(cols | d1 | d2)
  while free:
    bit  = free & (-free)   # lowest set bit = pick a column
    free ^= bit
    solve(cols|bit, (d1|bit)<<1, (d2|bit)>>1)

Here free & (-free) isolates the lowest available column in one operation; shifting d1 left and d2 right each recursion automatically slides the diagonal masks to the next row (a queen's diagonal influence moves one file per rank). This eliminates the explicit r+c / r−c index math and array marking:

  • State is three integers — no arrays to push/pop, so no memory traffic.
  • Conflict detection, candidate generation, and the undo are all register operations.
  • The lowest-set-bit iteration (free &= free − 1 to clear) visits exactly the legal columns, skipping conflicts without a per-column branch.

This is the workhorse behind record-setting counts: the n = 27 result (234,907,967,154,122,528 solutions) used a symmetry-reduced, FPGA-and-CPU bitmask search. On a laptop the bitmask version counts n = 15 in well under a second where the naive array version takes many times longer.

When to Reach for Backtracking (and When Not To)

Backtracking wins precisely when the problem is a constraint-satisfaction search whose constraints let you reject partial assignments early. N-Queens is the poster child: a single conflicting placement invalidates an exponential subtree. The same machinery solves Sudoku, graph coloring, the knight's tour, subset-sum/partition, crossword filling, and constraint solvers' core loops. The design questions are always: what is the partial state, what makes it invalid, and how cheaply can I detect invalidity?

  • Use it when constraints prune hard and you need all solutions or a certified answer (SAT/CSP solvers are industrial backtracking with clause learning).
  • Add heuristics for hard instances: minimum-remaining-values (place in the most-constrained row first) and forward checking / constraint propagation can cut the tree by orders of magnitude — this is how real Sudoku and scheduling solvers work.
  • Don't use it to merely count N-Queens at large N — counting is intractable; use it to find a solution or enumerate small N.
  • Prefer the closed form if you need only one solution: an explicit O(N) construction (Hoffman–Loessi–Moore, 1969, with cases on N mod 6) places all queens directly with no search at all.

Backtracking is a strategy, not a data structure — its speed lives entirely in the pruning predicate and the variable/value ordering.

Pitfalls, Edge Cases, and Variants

Small-N surprises. There are no solutions for N = 2 and N = 3 — a correct solver must return an empty set, not loop or error. N = 1 trivially has one solution. A frequent bug is assuming a solution always exists.

  • Diagonal index off-by-one: forgetting the + (N−1) offset on r − c yields negative array indices and silent corruption. The valid ranges are r+c ∈ [0, 2N−2] and r−c+N−1 ∈ [0, 2N−2] — both need 2N−1 slots.
  • Forgetting to undo: the backtrack step must clear exactly the column and two diagonals you set; leaking marks pollutes sibling branches and drops valid solutions.
  • Symmetry double-counting: the board has 8 symmetries (4 rotations × reflection). The 92 solutions for N = 8 reduce to 12 fundamental ones; if you want distinct solutions you must canonicalize, and record counts differ by a factor of up to 8 (some solutions are self-symmetric, so it isn't a clean ÷8).
  • Iterative version: the recursion can overflow the stack for large N in some languages; an explicit stack or the tail-shaped bitmask loop avoids it.

Variants worth knowing: counting (the OEIS A000170 sequence, still open past n = 27); the n-Queens completion problem — given a partial placement, can it be extended? — which was proven NP-complete (Gent, Jefferson, Nightingale, 2017), a sharp reminder that the general constrained version is genuinely hard; the toroidal (modular) queens; and peaceable/coexisting queens. Placing one solution is easy; completing an adversarial partial board is not.

Strategies for the N-Queens problem and their real costs
ApproachIdeaTimeSpaceFinds
Brute forceTry all C(N²,N) placementsO(N²ᶜʰᵒᵒˢᵉ N)O(N)All, absurdly slow
Permutation searchOne queen per row, check diagonalsO(N!)O(N)All solutions
Backtracking + pruningDFS, prune on first conflictO(N!) worst, far less in practiceO(N)All / first
Bitmask backtracking3 ints track col + 2 diagonalsO(N!) w/ tiny constantO(N)All / first
Explicit constructionClosed-form formula (n≥4)O(N)O(N)One solution only

Frequently asked questions

Why not just brute-force all placements?

There are C(N², N) ways to drop N queens on the board — 4.4 billion for N = 8, and it explodes hyper-exponentially. Backtracking's row-per-queen model plus column/diagonal pruning reduces this to a permutation tree of at most N! leaves, and pruning trims that tree to a few thousand nodes for N = 8. Brute force never reaches interesting N.

What's the actual time complexity?

The worst-case bound for enumerating all solutions is O(N!), because after column-distinctness each row has one fewer available column (N, then N−1, …). Diagonal pruning shrinks the explored tree enormously in practice but does not lower the asymptotic class — no polynomial bound is known, and the solution count itself grows super-exponentially. Space is Θ(N).

How do you check a diagonal conflict in O(1)?

Two queens share the ╱ anti-diagonal iff their r+c sums are equal, and the ╲ main diagonal iff their r−c differences are equal. Keep one boolean set (or bitmask) for occupied r+c values and another for occupied r−c values, indexing the latter as r−c+N−1 to stay non-negative. Each needs 2N−1 slots, and lookups are O(1).

What does the bitmask optimization actually buy you?

It replaces array marking and index arithmetic with three integer registers — columns, ╱-diagonals, ╲-diagonals. Candidate columns are FULL & ~(cols|d1|d2); you pick the lowest set bit with x & −x and recurse with the diagonal masks shifted left/right by one to advance a row. Everything is register-speed with no memory allocation, giving multi-fold constant-factor speedups that make counting n = 15+ feasible.

When does N-Queens become genuinely hard, not just tedious?

Finding one solution or enumerating small N is easy. But the N-Queens completion problem — given some queens already placed, can the board be finished? — is NP-complete (Gent et al., 2017). And exact solution counting has no known efficient algorithm; the count for n = 27 was the frontier as of 2016, computed with heavy parallel/FPGA search.

How is this backtracking pattern used in real systems?

The identical prune-and-recurse skeleton powers Sudoku solvers, graph/register coloring, constraint (CSP) and SAT solvers with backjumping and clause learning, automated scheduling, and puzzle/crossword generators. Production solvers add minimum-remaining-values ordering and forward checking / constraint propagation on top of the same backtracking core to cut the search tree by orders of magnitude.