Mid-level 14 min · March 05, 2026

N-Queens Backtracking — Why Your Solver Timed Out at N=16

Backtracking with boolean arrays hit 1.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
Plain-English First

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.

io/thecodeforge/nqueens/NQueensCheck.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package io.thecodeforge.nqueens;

// Check if placing queen at (row, col) is valid given existing queens
public class NQueensCheck {
    public static boolean isValid(int[] queens, int row, int col) {
        for (int i = 0; i < row; i++) {
            int placedCol = queens[i];
            int placedRow = i;
            if (placedCol == col ||
                placedCol - placedRow == col - row ||
                placedCol + placedRow == col + row) {
                return false;
            }
        }
        return true;
    }
}
Forge Tip:
The diagonal checks use two invariants: (col - row) is constant for the main diagonal, (col + row) for the anti-diagonal. This is the key to O(1) conflict detection.
Production Insight
Constraint solvers in production schedulers fail when early pruning is weak.
Weak pruning means exploring millions of dead branches — runtime jumps from seconds to hours.
Rule: invest in cheap, aggressive pruning checks before any recursion depth grows.
Key Takeaway
N-Queens is a constraint satisfaction problem.
Pruning is everything — not just recursion.
Diagonal invariants let you check conflicts in O(1).
When to Use Backtracking vs Other Approaches
IfN < 8, need all solutions
UseClassic backtracking with arrays is enough; bitmask overhead not justified
IfN >= 15, need one solution fast
UseUse bitmask optimisation; it cuts constants by 10-50x
IfSymmetry reduction needed
UseAdd rotation/reflection detection to avoid duplicate solutions

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

. 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.

io/thecodeforge/nqueens/PrintBoard.javaJAVA
1
2
3
4
5
6
7
8
9
10
public static void printBoard(int[] queens) {
    int n = queens.length;
    for (int row = 0; row < n; row++) {
        StringBuilder sb = new StringBuilder();
        for (int col = 0; col < n; col++) {
            sb.append(queens[row] == col ? 'Q' : '.').append(' ');
        }
        System.out.println(sb);
    }
}
Output
. Q . .
. . . Q
Q . . .
. . Q .
Production Insight
Visualising small N solutions is a fast sanity check during development. If your solver doesn't produce these two canonical patterns for N=4, the conflict detection logic is buggy. Always validate with N=4 before scaling up.
Key Takeaway
A valid N-Queens solution is a permutation of columns. The classic N=4 solutions are [1,3,0,2] and [2,0,3,1] after rotation/reflection.

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; } } ```

io/thecodeforge/nqueens/BruteForceNQueens.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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;
        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;
    }
}
Don't Use This in Production
Brute force by permutation still does O(N!) full board checks. For N=12 that's 479 million checks — your laptop will cry. Always use backtracking with pruning.
Production Insight
A naive constraint solver in production that enumerates all permutations will timeout once input size exceeds 10.
We've seen this in job scheduling engines that generate all valid sequences instead of backtracking.
Rule: if your solver enumerates complete states before checking constraints, refactor to incremental pruning.
Key Takeaway
Brute force explores complete boards, even when early placements are invalid.
For N=15, it's 1.3 trillion checks — literally never finishes.
Always prune as early as possible.

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.

```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.

io/thecodeforge/nqueens/BacktrackingNQueens.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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;
    }
}
Backtracking Visualised as a Maze
  • 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
Production Insight
Boolean arrays are fine for N up to 30, but beyond that cache misses degrade performance.
In real constraint solvers (e.g., time-tabling), they use bitsets or bitmasks to pack state into registers.
Rule: when N exceeds 64, switch to bitsets (Java BitSet or Kotlin).
Key Takeaway
Backtracking prunes at the first conflict, not after building the full board.
O(1) conflict checks via boolean arrays make each step cheap.
For N up to 15, classic backtracking is the right tool.
When Classic Backtracking Is Enough
IfN <= 15, need all solutions
UseClassic backtracking with boolean arrays works fine; no optimisation needed
IfN between 16 and 32
UseUse bitmask to fit state into 64-bit registers; speeds up inner loop 10x
IfN > 32
UseNeed symmetry breaking or parallel search; classic backtracking too slow

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.

io/thecodeforge/nqueens/DiagonalDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package io.thecodeforge.nqueens;

public class DiagonalDemo {
    // Show the invariant for an 8x8 board
    public static void main(String[] args) {
        int n = 8;
        System.out.println("Main diagonal (row - col): ranges from -7 to 7");
        System.out.println("Shifted index = row - col + n - 1, range 0..14");
        System.out.println("Anti-diagonal (row + col): ranges from 0 to 14");
        
        int row = 3, col = 5;  // example queen
        int d1Index = row + col;          // anti-diagonal
        int d2Index = row - col + n - 1;  // main diagonal
        System.out.println("Queen at (3,5): d1=" + d1Index + ", d2=" + d2Index);
    }
}
Output
Main diagonal (row - col): ranges from -7 to 7
Shifted index = row - col + n - 1, range 0..14
Anti-diagonal (row + col): ranges from 0 to 14
Queen at (3,5): d1=8, d2=0
Off-by-one trap
The main diagonal index formula 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.
Production Insight
In production constraint solvers, diagonal invariants are the primary reason backtracking scales.
A common bug: indexing the diagonal arrays with wrong size (2n instead of 2n-1) causes silent buffer overflow in C/C++.
Rule: always verify array sizes: diag1 size = 2n-1, diag2 size = 2n-1.
Key Takeaway
Queen diagonals follow simple arithmetic invariants.
Index shift turns negative indices into valid array positions.
Get the size right: 2N - 1, not 2N.

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().

```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.

io/thecodeforge/nqueens/BitmaskNQueens.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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;
            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;
        }
    }
}
Bitmask as a Sliding Window of Attacks
  • 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
Production Insight
In production constraint solvers (e.g., SAT solvers, SMT solvers), bitmasking is the default for small domains.
The inner loop speed gain from register-only state is often 10-100x over array approaches.
Trade-off: code is less readable, so document the mask shifting logic clearly.
Key Takeaway
Bitmask replaces three arrays with three integers.
Diagonal shifts happen automatically on recursion — no index arithmetic needed.
For N up to 64, this is the fastest known backtracking approach.

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.

Production Insight
We've seen teams split over readability vs. performance. In a production scheduler, the bitmask version cut runtime from 45s to 2s for N=20. But the code needed extensive comments for maintainers. Rule: profile first, then optimize only the hot path.
Key Takeaway
Bitmask wins on speed and compactness for N≤64. Set-based wins on readability and unbounded size. Choose based on your team and performance requirements.

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; } } }; ```

Key differences from Java
  • unsigned long long avoids sign issues.
  • __builtin_ctzll (GCC/Clang) counts trailing zeros; MSVC uses _BitScanForward64.
  • No extra allMask mask 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).

io/thecodeforge/nqueens/BitmaskNQueens.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <vector>
#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;
            int col = __builtin_ctzll(bit);
            current.push_back(col);
            backtrack(row + 1,
                      cols | bit,
                      (diag1 | bit) >> 1,
                      (diag2 | bit) << 1,
                      current, results);
            current.pop_back();
            available ^= bit;
        }
    }
};
Production Insight
In C++ production systems (game engines, embedded solvers), bitmask optimizations are standard because of tight loop requirements. The __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.
Key Takeaway
C++ bitmask solver mirrors Java logic but uses 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.

io/thecodeforge/nqueens/ComplexityDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package io.thecodeforge.nqueens;

// Quick runtime comparison (not production benchmarking)
public class ComplexityDemo {
    public static void main(String[] args) {
        for (int n = 8; n <= 16; n += 4) {
            long start = System.currentTimeMillis();
            new BacktrackingNQueens(n).solve();
            long t1 = System.currentTimeMillis() - start;
            
            start = System.currentTimeMillis();
            new BitmaskNQueens(n).solve();
            long t2 = System.currentTimeMillis() - start;
            
            System.out.printf("N=%d: backtracking=%dms, bitmask=%dms%n", n, t1, t2);
        }
    }
}
Output
N=8: backtracking=1ms, bitmask=0ms
N=12: backtracking=320ms, bitmask=5ms
N=16: backtracking=4802ms, bitmask=197ms
Interview Cue: Symmetry Reduction
If the interviewer asks about optimising further, mention that by placing the first queen only in half of the first row's columns (exploiting reflection symmetry), you can cut the search space in half. Combine with rotation to get ~8x speedup.
Production Insight
In production constraint solvers, factorial complexity is a showstopper for N > 30.
Real solvers use heuristics (minimum remaining values) to pick the most constrained variable first.
Rule: N-Queens is a toy model; in practice you need SAT solvers or CSP algorithms.
Key Takeaway
Time complexity is O(N!) in worst case for any exact solver.
Backtracking doesn't reduce the worst case, but it makes the average case practical.
In interviews, focus on pruning logic — not the factorial bound.

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).

NAll SolutionsFundamental Solutions
111
200
300
421
5102
641
7406
89212
935246
1072492
112680341
12142001787
13737129233
1436559645752

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.

Production Insight
When validating a solver in production, always compare the solution count against known values. For N=12, a correct solver must produce exactly 14,200 solutions. If your bitmask solver returns 14,200, you know the pruning logic works. Use this as a regression test.
Key Takeaway
Solution counts are known up to N=27 (all) and N=26 (fundamental). Use these as test oracles for your implementation.

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.

  1. 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.
  2. [https://leetcode.com/problems/n-queens-ii/](https://leetcode.com/problems/n-queens-ii/)
  3. 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.
  4. [https://leetcode.com/problems/n-queens/](https://leetcode.com/problems/n-queens/)
  5. 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.
  6. [https://codeforces.com/problemset/problem/1243/B2](https://codeforces.com/problemset/problem/1243/B2)
  7. 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.
  8. [https://leetcode.com/problems/sudoku-solver/](https://leetcode.com/problems/sudoku-solver/)
  9. 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.
  10. [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.

Production Insight
Practice problems like these appear in coding interviews at top tech companies. The bitmask technique specifically impresses interviewers because it shows you understand the low-level performance implications. But don't lead with it — use array-based first, then mention bitmask as an optimization.
Key Takeaway
Backtracking with diagonal constraints is a transferable skill. N-Queens II (count) and Sudoku are the most common follow-ups in interviews.
● Production incidentPOST-MORTEMseverity: high

Constraint Solver Times Out at N=16 in Production Job Scheduler

Symptom
A batch processing system that allocated resources to parallel jobs using a simple backtracking engine started timing out consistently when the number of jobs reached 16. The system had worked fine for up to 12 jobs for months.
Assumption
The team assumed the algorithm's performance scaled linearly with input size, as it did for smaller N. They didn't profile the actual execution.
Root cause
The backtracking implementation used boolean arrays and O(N) conflict checks per placement (iterating over all prior queens). The O(N!) growth meant N=16 was ~1.3 trillion nodes vs N=12's 479 million. Combined with poor constant factors (iterating over all previous queens), the solver hit the 5-minute timeout.
Fix
Replaced the solver with a bitmask-based version (O(1) conflict checks) and added a timeout with graceful fallback to a heuristic min-conflicts local search. Also limited the input size to N<=12 for exact scheduling, using approximation for larger loads.
Key lesson
  • 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.
Production debug guideDiagnose why your constraint solver is slow or producing wrong results5 entries
Symptom · 01
Solver completes but finds zero solutions when solutions exist
Fix
Check that all pruning arrays are properly unmarked after recursion. Print all placements at leaf nodes — if partial boards reach leaf but aren't added, the bug is in the solution recording code.
Symptom · 02
Solver finds duplicate solutions
Fix
Add symmetry breaking — restrict first queen to first half of first row. Or deduplicate with a canonical representation (sorted vector of queen coordinates).
Symptom · 03
Solver is extremely slow for N > 12 (takes minutes)
Fix
Profile with 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.
Symptom · 04
Solver crashes with stack overflow
Fix
Set JVM stack size (-Xss). For N up to 30, stack depth is max 30 — should not overflow. Check for infinite recursion: are you incrementing row correctly? Is base condition row==N?
Symptom · 05
Bitmask solution finds wrong number of solutions for N=8 (not 92)
Fix
Verify allMask calculation: (1L << N) - 1. For N=8, allMask = 255. If using int instead of long for N>32, overflow causes bit 31 to be sign bit. Also verify shift directions: diag1 >> 1, diag2 << 1.
★ Quick Debug Cheat Sheet for N-Queens BacktrackingUse these commands and checks when your backtracking solver doesn't behave as expected.
Solver produces no solutions
Immediate action
Add print statement at base case to confirm leaf nodes are reached.
Commands
System.out.println("Solution: " + Arrays.toString(state));
Check if conflict detection is too aggressive: temporarily remove pruning, see if all permutations appear.
Fix now
Insert debug print at start of backtrack method: print row, current state, and available columns.
Solver returns too many solutions (includes invalid ones)+
Immediate action
Verify conflict check logic for all three constraints: same column, main diagonal, anti-diagonal.
Commands
Print the three boolean arrays at each placement to see if marks are correct.
Write a separate validator that checks a completed board independently.
Fix now
Most common bug: not using (row - col + N - 1) for diagonal index, causing different queens to collide in the array.
Bitmask version crashes at N=32+
Immediate action
Check if you used int for bitmask instead of long.
Commands
System.out.println("allMask = " + Long.toBinaryString(allMask));
Verify mask length: allMask == (1L << N) - 1 with N up to 64.
Fix now
Replace int with long, and use Long.numberOfTrailingZeros().
Algorithm Comparison
ApproachTime ComplexitySpace ComplexityN=12 RuntimeReadability
Brute Force PermutationsO(N! * N)O(N)> 1 hourEasy (but slow)
Classic BacktrackingO(N!) avg ~O(2ⁿ)O(N)~320 msHigh
Bitmask BacktrackingO(N!) with low constantO(N)~5 msMedium (bit twiddling)

Key takeaways

1
N-Queens is the archetype of constraint satisfaction
master it and you understand CSPs
2
Pruning is the sole reason backtracking works
detect conflicts as early as possible
3
Diagonal invariants (row+col, row-col) are universal in board problems
4
Bitmask optimisation gives 10-100x speed by keeping state in registers
5
Fallback to classic backtracking for clarity; mention bitmask as an optimisation during interviews

Common mistakes to avoid

5 patterns
×

Using brute force (all permutations) in an interview

Symptom
You spend time generating permutations and validating entire boards, missing the pruning insight.
Fix
Always start with backtracking — generate permutations implicitly by choosing one column per row and undoing on backtrack.
×

Forgetting to unmark arrays/masks after recursion

Symptom
You get fewer solutions than expected because the state is not properly reset.
Fix
Always unmark in the reverse order of marking, right after the recursive call returns. Use try-finally in languages with exceptions.
×

Incorrect diagonal array sizes

Symptom
ArrayIndexOutOfBoundsException for large N, or silent incorrect checks.
Fix
diag1 size = 2N - 1, diag2 size = 2N - 1. Use row+col for diag1 index, row-col+N-1 for diag2 index.
×

Using int instead of long for bitmask (queue N>32)

Symptom
Overflow and incorrect solutions for N > 32.
Fix
Use long (64-bit) for up to N=64. For N>64, use java.util.BitSet or multiple longs.
×

Not exploiting symmetry to reduce search space

Symptom
Your algorithm generates 8x more solutions than necessary due to rotations/reflections.
Fix
Place first queen only in first half of first row's columns, then deduplicate with a canonical form check.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how backtracking solves N-Queens. Time complexity?
Q02SENIOR
How would you optimise the backtracking for N=50?
Q03SENIOR
Write code to print all solutions for N-Queens using backtracking. (Whit...
Q04SENIOR
What's the difference between backtracking and brute force in this conte...
Q01 of 04SENIOR

Explain how backtracking solves N-Queens. Time complexity?

ANSWER
Backtracking places queens row by row. For each row, try every column that does not conflict with placed queens. Use three boolean arrays (cols, diag1, diag2) for O(1) conflict check. If a placement leads to no solution deeper, undo and try next column. Worst-case time is O(N!) because each row has up to N choices and pruning doesn't change theoretical bound. Average case is far better due to early conflicts. Space O(N) for recursion stack.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What's the fastest way to solve N-Queens for N=100?
02
Why is backtracking called 'backtracking'?
03
Does N-Queens have a formula for the number of solutions?
04
Can N-Queens be solved with DP?
🔥

That's Greedy & Backtracking. Mark it forged?

14 min read · try the examples if you haven't

Previous
Backtracking Introduction
5 / 13 · Greedy & Backtracking
Next
Sudoku Solver