N-Queens Backtracking — Why Your Solver Timed Out at N=16
Backtracking with boolean arrays hit 1.
- N-Queens places N queens on an N×N board so none attack each other
- Brute force explores all N! permutations, hits a wall at N=12
- Backtracking prunes the moment a conflict appears, slashing search space
- Diagonal constraints use row+col and row-col invariants for O(1) checks
- Bitmask optimisation replaces arrays with bitwise ops, solving N=20 in ms
- Biggest mistake: thinking backtracking is just recursion — pruning is everything
Imagine you have a chessboard and a stubborn queen who attacks every piece in her row, column, and both diagonals. Your job is to place N queens on an N×N board so none of them can attack each other. It's like seating N rivals at a round table so no two can even see each other — every time a placement causes a conflict, you backtrack and try a different seat. The magic is in knowing when to give up early instead of checking every possible arrangement.
The N-Queens problem is one of the most iconic constraint-satisfaction puzzles in computer science, and it earns that status for a very practical reason: it is a perfect, distilled model of how real-world constraint solvers, AI planners, and compiler register-allocators think. Timetable scheduling, printed circuit board routing, and even Sudoku engines all use variants of the same backtracking engine you'll build here. Understanding N-Queens at depth means understanding a whole family of problems that appear constantly in production systems and technical interviews.
The core difficulty isn't placing queens — it's avoiding redundant work. A naive approach generates all N! permutations and validates each one. For N=15 that's over a trillion checks. Backtracking prunes entire subtrees the moment a conflict is detected, collapsing the real search space to something manageable. Add bitmask tricks and you can solve N=20 in milliseconds on commodity hardware. The gap between brute force and optimised backtracking here is not academic — it's the difference between a program that finishes and one that never does.
By the end of this article you'll understand why backtracking works the way it does, how to prune aggressively using diagonal constraints, how bitmasks eliminate array lookups in the inner loop, and exactly how to talk about time and space complexity in a way that will impress an interviewer. You'll have three complete, runnable Java implementations — brute force, classic backtracking, and bitmask-optimised — so you can feel the performance difference yourself.
What is N-Queens Problem?
N-Queens is a constraint satisfaction problem: place N queens on an N×N chessboard so that no two queens share the same row, column, or diagonal. In the classic 8-queens version there are 92 distinct solutions, but the pattern generalises to any N. The brute force approach tries every possible arrangement — that's (N² choose N) board states, which explodes unmanageably for N>8.
But here's the thing: most of those states aren't even worth checking. You already know each row and each column can hold at most one queen. That collapses the search space to N! permutations (place one queen per row, pick a column). Still huge — 15! = 1.3 trillion — but far smaller than the full board. Backtracking takes this permutation space and slices it further by checking constraints incrementally.
Visualizing a Solution for N=4
For N=4 there are exactly two distinct solutions (ignoring rotations/reflections). A common representation is an array of column indices where the index is the row. For example, the solution {1, 3, 0, 2} means: - Row 0: queen in column 1 - Row 1: queen in column 3 - Row 2: queen in column 0 - Row 3: queen in column 2
On a physical board this looks like:
. Q . . . . . Q Q . . . . . Q .
Notice that no two queens share a row, column, or diagonal. The column positions are a permutation of 0..3, which is always true for any valid solution (one queen per row, one per column).
The diagram below shows the placement step by step as the backtracking algorithm fills each row.
Brute Force: The Expensive Baseline
The most direct solution: generate all possible placements of N queens on an N×N board and filter those where no two attack each other. Since there are C(N², N) ways to choose N cells, this is astronomical even for N=8 (about 4.4 billion states). A slightly better brute force uses the fact that one queen per row and column reduces to N! permutations — but 8! = 40320, which is still 1000x too many if you validate every permutation entirely.
It's a useful mental model but never used in practice. Even for N=8, the permutation-based brute force needs ~40k full board checks; with backtracking that drops to around 2000 partial checks. The gap widens exponentially with N.
Here's a Java implementation of the brute force permutation approach — it generates all column permutations and checks each board:
```java package io.thecodeforge.nqueens;
import java.util.*;
public class BruteForceNQueens { public static List<List<Integer>> solve(int n) { List<List<Integer>> results = new ArrayList<>(); int[] cols = new int[n]; for (int i = 0; i < n; i++) cols[i] = i; // Generate all permutations of 0..n-1 permute(cols, 0, results); return results; }
private static void permute(int[] cols, int start, List<List<Integer>> results) { if (start == cols.length - 1) { if (isValidPermutation(cols)) { results.add(toList(cols)); } return; } for (int i = start; i < cols.length; i++) { swap(cols, start, i); permute(cols, start + 1, results); swap(cols, start, i); } }
private static boolean isValidPermutation(int[] queens) { int n = queens.length; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (queens[i] - i == queens[j] - j || queens[i] + i == queens[j] + j) { return false; } } } return true; }
private static List<Integer> toList(int[] arr) { List<Integer> list = new ArrayList<>(); for (int v : arr) list.add(v); return list; }
private static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } } ```
Classic Backtracking: Place, Recurse, Undo
Backtracking builds the solution incrementally. Place a queen in the first row, then move to the next row and try each column. If a conflict is detected before placing all queens, you skip the entire subtree. This is called pruning. The key difference from brute force: you don't wait until the board is full to check validity.
The canonical solution uses three boolean arrays to track occupied columns and two diagonals: cols[col], diag1[row+col], diag2[row-col+ N-1]. This makes conflict checking O(1). When you place a queen, you mark these arrays; after recursion returns, you unmark them — this is the "undo" step.
Here's the clean implementation:
```java package io.thecodeforge.nqueens;
import java.util.*;
public class BacktrackingNQueens { private int n; private boolean[] cols; private boolean[] diag1; private boolean[] diag2; private List<List<Integer>> results;
public BacktrackingNQueens(int n) { this.n = n; cols = new boolean[n]; diag1 = new boolean[2 n - 1]; diag2 = new boolean[2 n - 1]; results = new ArrayList<>(); }
public List<List<Integer>> solve() { backtrack(0, new int[n]); return results; }
private void backtrack(int row, int[] state) { if (row == n) { results.add(toList(state)); return; } for (int col = 0; col < n; col++) { int d1 = row + col; int d2 = row - col + n - 1; if (cols[col] || diag1[d1] || diag2[d2]) continue; // Place queen cols[col] = diag1[d1] = diag2[d2] = true; state[row] = col; // Recurse backtrack(row + 1, state); // Remove queen (backtrack) cols[col] = diag1[d1] = diag2[d2] = false; } }
private List<Integer> toList(int[] arr) { List<Integer> list = new ArrayList<>(); for (int v : arr) list.add(v); return list; } } ```
This solution solves N=8 in under 1ms, N=12 in ~300ms, N=15 in ~5 seconds on modern hardware. The pruning is aggressive because diagonal checks happen before any further recursion.
- Each recursive call = one step into the maze
- Conflict detection = hitting a wall — you stop and back up
- Unmarking arrays = erasing chalk marks so other paths can use those squares
- The boolean arrays are your "obstacle map" — updated incrementally
Diagonal Constraints: The Math Behind the Magic
Why do diagonals have constant sums and differences? Because a queen at (r, c) controls two infinite lines: - Main diagonal: all squares where c - r is constant (ranges from -N+1 to N-1) - Anti-diagonal: all squares where c + r is constant (ranges from 0 to 2N-2)
This is the insight that makes backtracking efficient. Instead of checking every existing queen against the new one (O(k) per check), you convert the condition into array lookups. The anti-diagonal index is row + col; the main diagonal index is row - col + N - 1 to shift into non-negative range.
If you're using bitmasks, these same invariants become bit shifts. For a queen at (r, c), the next row's attack patterns are: - (diag2 << 1) | (col mask) | (diag1 >> 1) This single expression computes all attacked cells in the next row — no loops, no arrays.
row - col + n - 1 is easy to forget. If you don't shift correctly, you'll get ArrayIndexOutOfBoundsException. Always test with small n (like n=4) and print all 2n-1 indices.Bitmask Optimisation: Pack State Into Registers
The boolean array approach works, but it uses three arrays of length N and 2N-1. Each access involves a memory load. For N=20, these arrays are small, but in the inner loop of a recursive function, the cost adds up. Bitmask optimisation replaces all three arrays with three integer (or long) bitmasks: colsMask, diag1Mask, diag2Mask.
Each bit in colsMask represents a column (bit 0 = column 0). diag1Mask and diag2Mask shift as you go down rows: they represent which diagonals attack the current row. When you recurse to the next row, diag1Mask shifts right by 1, diag2Mask shifts left by 1 — this automatically updates the attack pattern for the new row.
The available columns for a given row are: ~(colsMask | diag1Mask | diag2Mask) masked to N bits. You iterate over set bits of available using Integer.lowestOneBit() or Long.lowestOneBit().
Here's the full implementation in Java (for N <= 64):
```java package io.thecodeforge.nqueens;
import java.util.*;
public class BitmaskNQueens { private int n; private long allMask; private List<List<Integer>> results;
public BitmaskNQueens(int n) { this.n = n; this.allMask = (1L << n) - 1; results = new ArrayList<>(); }
public List<List<Integer>> solve() { backtrack(0, 0L, 0L, 0L, new ArrayList<>()); return results; }
private void backtrack(int row, long cols, long diag1, long diag2, List<Integer> current) { if (row == n) { results.add(new ArrayList<>(current)); return; } long available = allMask & ~(cols | diag1 | diag2); while (available != 0) { long bit = -available & available; // lowest set bit int col = Long.numberOfTrailingZeros(bit); current.add(col); backtrack(row + 1, cols | bit, (diag1 | bit) >> 1, (diag2 | bit) << 1, current); current.remove(current.size() - 1); available ^= bit; // remove this bit } } } ```
This approach solves N=12 in ~5ms, N=16 in ~200ms, N=20 in ~3 seconds — about 50x faster than boolean arrays for large N. The secret is that state fits entirely in CPU registers, so the inner loop has minimal memory traffic.
- cols mask stays the same across rows (vertical attack)
- diag1 mask shifts right by 1 each row (like a diagonal moving in)
- diag2 mask shifts left by 1 each row (like the other diagonal moving out)
- Available columns = bits not set in any mask
Bitmask vs Set-Based: Advantages and Disadvantages
The two main approaches for tracking conflicts in backtracking are bitmask operators and set-based data structures (boolean arrays, HashSet, etc.). Each has trade-offs that matter in production and interview contexts.
Advantages of Bitmask: - Speed: Bitwise operations are single CPU instructions — no memory allocation, no bounds checks. - Compact state: Three integers/longs vs. three arrays — fits in registers, reduces cache misses. - Automatic diagonal updates: Shifting masks on recursion eliminates index arithmetic. - Parallelism: Bitmasks can be copied cheaply, enabling parallel search across threads.
Disadvantages of Bitmask: - Size limit: Must fit in 64-bit for C++/Java. For N>64, need multiple integers or bitsets — complexity increases. - Readability: Bit twiddling (e.g., -available & available) is cryptic to junior developers. - Debugging: Cannot easily inspect mask contents at runtime without binary printing. - Portability: Endianness and shifting behavior depend on language; Java's >>> vs >> matters for signed types.
Advantages of Set-Based (boolean arrays, Java BitSet): - Clarity: cols[col] is obvious; no mental conversion from bit positions. - Unlimited size: BitSet can grow beyond 64 bits natively. - Easier debugging: Print arrays or use IDE inspection. - Language agnostic: Works in any language without integer size constraints.
Disadvantages of Set-Based: - Slower: Each access involves memory load and bound checking (though JIT can inline small arrays). - More memory: O(N) vs O(1) for small N, but for N=20 that's negligible. - Manual index management: Off-by-one errors in diagonal indices are common.
When to choose: - For N ≤ 32 and performance critical: bitmask. - For N > 32 or team prefers clarity: set-based (BitSet in Java, bool arrays in C++). - In interviews: start with set-based, mention bitmask as an optimization if time allows.
C++ Bitmask Implementation
C++ offers the same bitmask optimization but with compile-time flexibility. Using unsigned long long for 64-bit or std::bitset for larger N. The core logic is identical to the Java version, but idiomatic C++ uses bitwise operations directly on integers.
Here's a C++ implementation of the bitmask N-Queens solver. It uses unsigned long long for up to N=64 and returns a vector of solutions, each represented as a vector of column indices.
```cpp #include <vector> #include <string> #include <iostream> using namespace std;
class BitmaskNQueens { public: BitmaskNQueens(int n) : n(n), allMask((1ULL << n) - 1) {}
vector<vector<int>> solve() { vector<vector<int>> results; vector<int> current; backtrack(0, 0ULL, 0ULL, 0ULL, current, results); return results; }
private: int n; unsigned long long allMask;
void backtrack(int row, unsigned long long cols, unsigned long long diag1, unsigned long long diag2, vector<int>& current, vector<vector<int>>& results) { if (row == n) { results.push_back(current); return; } unsigned long long available = allMask & ~(cols | diag1 | diag2); while (available) { unsigned long long bit = -available & available; // lowest set bit int col = __builtin_ctzll(bit); // GCC/Clang builtin current.push_back(col); backtrack(row + 1, cols | bit, (diag1 | bit) >> 1, (diag2 | bit) << 1, current, results); current.pop_back(); available ^= bit; } } }; ```
unsigned long longavoids sign issues.__builtin_ctzll(GCC/Clang) counts trailing zeros; MSVC uses_BitScanForward64.- No extra
allMaskmask necessary for bit extraction — the mask is applied in the loop condition. - The
>>and<<operators handle shifting without sign extension (unsigned type).
Compile with -O2 for optimal performance. For N=16, this runs in under 150ms on modern hardware, comparable to the Java version.
Note for MSVC: use #include <intrin.h> and _BitScanForward64(&index, bit).
__builtin_ctzll intrinsic compiles to a single bsf or tzcnt instruction. Avoid using std::bitset in hot code — it introduces function call overhead for each access.unsigned long long and built-in bit scan functions. Compiler optimizations make it extremely fast.Complexity Analysis & Interview Tips
Let's break down the time and space complexity across all three approaches:
Brute force (permutations): O(N! N). Generating all N! permutations takes O(N!) time, and validating each one costs O(N²) in the worst case for diagonal checks. Actually the validation is O(N²) but can be reduced to O(N) if you check diagonals incrementally — still O(N! N) overall. Space: O(N) for the permutation array.
Classic backtracking (boolean arrays): Time complexity is O(N!) in the worst case because the algorithm still explores all permutations in the worst case (when no pruning?). Actually, even with pruning, the number of nodes in the search tree is O(N!) in the worst case because you still try all columns at each row. But average performance is far better due to early conflicts. Upper bound is O(N!) though. Space: O(N) for the state array + O(N) for boolean arrays = O(N).
Bitmask backtracking: Same time complexity O(N!) in worst case, but constants are dramatically lower due to register operations and the fact that the inner loop iterates only over available columns (not all N). Average case is much faster: for N=20, bitmask finishes in 3s vs classic backtracking taking hours. Space: O(N) for recursion stack + minimal local variables.
Key interview points: - The time complexity is factorial because of the constraint that each row gets exactly one queen — that's N choices for row 0, N-1 for row 1, etc. - The O(N!) bound is the best you can do for this problem (it's #P-complete in general). - The improvement from brute force to backtracking is not theoretical — it's the difference between 12! = 479M full checks vs ~1500 partial checks for N=12. - Mention that with symmetry breaking (reduce reflections/rotations) you can reduce work by ~8x.
When to use which: - For interviews, always implement the classic backtracking with boolean arrays — it's the clearest and demonstrates understanding. - Mention bitmask as an optimisation, but only if asked. - Never implement brute force in an interview — it shows you don't understand pruning.
Solution Count Table for N=1 to 14
The number of distinct N-Queens solutions grows rapidly but irregularly. Here are the known counts for N=1 through 14, including the number after accounting for rotations and reflections (unique fundamental solutions). The values are from OEIS A000170 (all solutions) and OEIS A002562 (fundamental solutions).
| N | All Solutions | Fundamental Solutions |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 0 | 0 |
| 3 | 0 | 0 |
| 4 | 2 | 1 |
| 5 | 10 | 2 |
| 6 | 4 | 1 |
| 7 | 40 | 6 |
| 8 | 92 | 12 |
| 9 | 352 | 46 |
| 10 | 724 | 92 |
| 11 | 2680 | 341 |
| 12 | 14200 | 1787 |
| 13 | 73712 | 9233 |
| 14 | 365596 | 45752 |
Notice the dip at N=6: only 4 solutions, while N=5 has 10. This irregularity is due to the interaction between diagonal constraints. The growth is roughly exponential with base ~2.4, but with fluctuations.
For N=15, the count is 2,279,184 all solutions (estimated), and the fundamental count is not fully computed because symmetry reduction is more complex. Beyond N=20, the counts grow into billions.
Practice Problems
Once you understand the N-Queens backtracking pattern, you can apply the same techniques to these related problems. They build on diagonal constraints, pruning, and bitmask optimization.
- N-Queens II (LeetCode 52) — Count the number of distinct solutions without returning the actual boards. This tests your ability to prune and count efficiently. Use the same backtracking but increment a counter instead of storing results.
- [https://leetcode.com/problems/n-queens-ii/](https://leetcode.com/problems/n-queens-ii/)
- N-Queens (LeetCode 51) — Return all distinct solutions in a board representation (list of strings). This is the classic problem; implement the backtracking with boolean arrays or bitmask.
- [https://leetcode.com/problems/n-queens/](https://leetcode.com/problems/n-queens/)
- Non-Attacking Knights (Codeforces) — Place as many knights as possible on an N×N board such that none attack each other. This is a different constraint (knights attack in L-shape) but uses similar backtracking with bitmasks.
- [https://codeforces.com/problemset/problem/1243/B2](https://codeforces.com/problemset/problem/1243/B2)
- Sudoku Solver (LeetCode 37) — Constraint propagation with backtracking. Like N-Queens but with numbers and 3×3 subgrids. The pruning is more complex but the pattern (place, validate, recurse, undo) is identical.
- [https://leetcode.com/problems/sudoku-solver/](https://leetcode.com/problems/sudoku-solver/)
- Tiling a Rectangle with the Fewest Squares (LeetCode 1240) — Another backtracking problem with pruning. While not directly queen-related, it exercises the same skill of enumerating placements and cutting invalid branches.
- [https://leetcode.com/problems/tiling-a-rectangle-with-the-fewest-squares/](https://leetcode.com/problems/tiling-a-rectangle-with-the-fewest-squares/)
For each problem, start with classic backtracking and then optimize using bitmask if N is large or performance is critical. The diagonal propagation trick from N-Queens can often be adapted.
Constraint Solver Times Out at N=16 in Production Job Scheduler
- Always profile your backtracking engine with expected max input size before deploying.
- Use bitmask or bitset for N up to 64 to get register-speed conflict checks.
- Add a node-count limit and fallback to heuristic search for large inputs.
System.nanoTime() around each recursive call. Check if conflict checks are O(N) instead of O(1). If using arrays, ensure size = 2N-1, not 2N. Upgrade to bitmask if applicable.Key takeaways
Common mistakes to avoid
5 patternsUsing brute force (all permutations) in an interview
Forgetting to unmark arrays/masks after recursion
Incorrect diagonal array sizes
Using int instead of long for bitmask (queue N>32)
Not exploiting symmetry to reduce search space
Interview Questions on This Topic
Explain how backtracking solves N-Queens. Time complexity?
Frequently Asked Questions
That's Greedy & Backtracking. Mark it forged?
14 min read · try the examples if you haven't