51. N-Queens

Place n queens on an n×n board so no two attack each other. The canonical approach recurses one row at a time and uses three sets — columns, diagonals, and anti-diagonals — to prune illegal placements in O(1) per check.

HardBacktrackingConstraint PropagationSet-based PruningTypeScript

PROBLEM What we're solving

Given an integer n, return all distinct solutions to the N-Queens puzzle: place n queens on an n×n chess board so that no two queens share a row, column, or diagonal.

Concrete example — n = 4: two solutions exist.

Solution 1        Solution 2
. Q . .           . . Q .
. . . Q           Q . . .
Q . . .           . . . Q
. . Q .           . Q . .

Output is an array of boards; each board is an array of strings, e.g. [".Q..","...Q","Q...","..Q."].

KEY IDEA One queen per row; track three conflict sets

Insight → Because each row must have exactly one queen, we recurse one row at a time. The only question is: which column? Two queens attack diagonally if they share row − col (the main diagonal) or row + col (the anti-diagonal). Storing both values in sets gives an O(1) conflict check— no board scanning needed. Place, recurse, remove. That's the whole algorithm.

RECIPE Row-by-row backtracking with three sets

  • 0 · Initialize. Create sets cols, diag (r-c), anti (r+c) and an array queens[]. All are empty.
  • 1 · Base case. If row === n, all queens are placed validly — serialize the board and push to results. Return.
  • 2 · Try each column. For col in 0…n-1, skip if cols, diag, or anti contains the relevant key — this position is attacked.
  • 3 · Place. Push col onto queens, add col, row-col, and row+col to the three sets.
  • 4 · Recurse. Call backtrack(row + 1). The next level will try every column for the next row.
  • 5 · Remove. Pop queensand delete the three keys from their sets. This is the “undo” that makes backtracking work — the state must be exactly as it was before step 3.
Classic confusion → learners sometimes forget that both diagonal directions need their own set. The main diagonal uses r - c (same value for cells running top-left to bottom-right) while the anti-diagonal uses r + c (same value top-right to bottom-left). Mixing them up or using only one will silently miss conflicts on one axis.

COST Complexity & alternatives

Brute force: try all placements
O(n!·n)
n! orderings × O(n) board check per leaf.
Set-pruned backtracking
O(n!)
O(1) conflict check; O(n) to serialize each solution.

Space is O(n) for the recursion stack and the three sets (each holds at most n entries). The output itself is output size — not counted in auxiliary space.

The number of solutions grows super-exponentially with n: n=4 has 2, n=8 has 92, n=14 has 365,596. In practice the pruning is extremely effective — the search tree is vastly smaller than n!.

Pattern transfer → the same row-by-row backtracking + set-pruning structure solves N-Queens II (just count solutions), Sudoku Solver (digits 1–9 per row/col/box via sets), Word Search(grid DFS with a visited set), and any “place items without mutual conflicts” puzzle.

RUN IT Place queens row by row, pruning with sets

step 0 / 79
STARTN-Queens with n=4. Place one queen per row — track cols, r-c, and r+c sets to detect conflicts.
1function solveNQueens(n: number): string[][] {
2 const results: string[][] = [];
3 const cols = new Set<number>();
4 const diag = new Set<number>(); // r - c (top-left → bottom-right)
5 const anti = new Set<number>(); // r + c (top-right → bottom-left)
6 const queens: number[] = []; // queens[r] = column of queen in row r
7
8 function backtrack(row: number): void {
9 if (row === n) {
10 // Build the board from the completed queen placement
11 results.push(
12 queens.map((c) =>
13 '.'.repeat(c) + 'Q' + '.'.repeat(n - c - 1)
14 )
15 );
16 return;
17 }
18 for (let col = 0; col < n; col++) {
19 if (cols.has(col) || diag.has(row - col) || anti.has(row + col)) continue;
20 // Place
21 queens.push(col);
22 cols.add(col);
23 diag.add(row - col);
24 anti.add(row + col);
25
26 backtrack(row + 1);
27
28 // Remove (backtrack)
29 queens.pop();
30 cols.delete(col);
31 diag.delete(row - col);
32 anti.delete(row + col);
33 }
34 }
35
36 backtrack(0);
37 return results;
38}
c0
c1
c2
c3
r0
.
.
.
.
r1
.
.
.
.
r2
.
.
.
.
r3
.
.
.
.
State
cols: {}
diag: {}
anti: {}
results: 0
row: 0 / 4
queens: []
Queen placed (chosen)Trying this cellAttacked cellBacktrack step
slowfast

TYPESCRIPT The solution, annotated

nQueens.ts
function solveNQueens(n: number): string[][] {
  const results: string[][] = [];
  const cols    = new Set<number>();
  const diag    = new Set<number>(); // r - c  (top-left → bottom-right)
  const anti    = new Set<number>(); // r + c  (top-right → bottom-left)
  const queens: number[] = [];       // queens[r] = column of queen in row r

  function backtrack(row: number): void {
    if (row === n) {
      // Build the board from the completed queen placement
      results.push(
        queens.map((c) =>
          '.'.repeat(c) + 'Q' + '.'.repeat(n - c - 1)
        )
      );
      return;
    }
    for (let col = 0; col < n; col++) {
      if (cols.has(col) || diag.has(row - col) || anti.has(row + col)) continue;
      // Place
      queens.push(col);
      cols.add(col);
      diag.add(row - col);
      anti.add(row + col);

      backtrack(row + 1);

      // Remove (backtrack)
      queens.pop();
      cols.delete(col);
      diag.delete(row - col);
      anti.delete(row + col);
    }
  }

  backtrack(0);
  return results;
}

Reading it block by block

Lines 2–5 — the conflict-tracking state. Three sets replace O(n) board scans: cols tracks occupied columns; diag stores row - col for main diagonals; anti stores row + col for anti-diagonals. queens[r] records which column row r chose — used only at leaves to serialize.
Lines 8–14 — base case: record a solution. When row === n every row has a queen; the placement is valid by construction (we never placed into a conflict). We serialize: for each row r, dot-pad around column queens[r] using string repetition.
Lines 15–17 — prune or skip. For each column candidate, the single if checks all three conflict axes simultaneously. If any set contains the relevant key, this cell is attacked — continue to the next column. No board scan.
Lines 18–22 — place the queen. Push the column into queens and record the three keys into their sets. Then recurse into row row + 1.
Lines 24–28 — undo (backtrack). After the recursive call returns, restore the state exactly as it was: pop queens, delete the three keys. This undo is the defining move of backtracking — the loop can then try the next column with a clean slate.
Complexity → Time: O(n!) in the worst case (each row prunes available columns). Space: O(n) auxiliary for the stack and sets (the output is separate). In practice the pruning makes it far faster than n! — n=8 explores only ~2,000 nodes out of 8! = 40,320.

INTERVIEWFollow-ups they'll ask

  • “N-Queens II: just count solutions?” Replace the results array with a counter; remove the board serialization entirely. Same backtracking, tiny change.
  • “Can you do it iteratively?” Simulate the call stack explicitly with an array of {row, col} choices, or use a classic iterative backtracking loop that increments a column index and backtracks when all columns are exhausted.
  • “Bit-manipulation version?” Track columns and diagonals as integer bitmasks; use lowbit = x & (-x) to isolate the next available column in a single instruction. N=15 runs in milliseconds.
  • “What if two queens can be in the same row?” The constraint that fixes one queen per row is what lets us recurse by row — removing it forces full 2D search (exponentially harder).
  • “Return the first solution only?” Return a boolean from backtrack; propagate true upward immediately on the first record, short-circuiting all remaining branches.

OPTIMAL Backtracking

nQueens.ts
function solveNQueens(n: number): string[][] {
  const results: string[][] = [];
  const cols    = new Set<number>();
  const diag    = new Set<number>(); // r - c  (top-left → bottom-right)
  const anti    = new Set<number>(); // r + c  (top-right → bottom-left)
  const queens: number[] = [];       // queens[r] = column of queen in row r

  function backtrack(row: number): void {
    if (row === n) {
      // Build the board from the completed queen placement
      results.push(
        queens.map((c) =>
          '.'.repeat(c) + 'Q' + '.'.repeat(n - c - 1)
        )
      );
      return;
    }
    for (let col = 0; col < n; col++) {
      if (cols.has(col) || diag.has(row - col) || anti.has(row + col)) continue;
      // Place
      queens.push(col);
      cols.add(col);
      diag.add(row - col);
      anti.add(row + col);

      backtrack(row + 1);

      // Remove (backtrack)
      queens.pop();
      cols.delete(col);
      diag.delete(row - col);
      anti.delete(row + col);
    }
  }

  backtrack(0);
  return results;
}
Complexity → Time: O(n!) in the worst case (each row prunes available columns). Space: O(n) auxiliary for the stack and sets (the output is separate). In practice the pruning makes it far faster than n! — n=8 explores only ~2,000 nodes out of 8! = 40,320.

ALT 1 Bitmask backtracking

Time O(n!) · Space O(n)

Replace the three Sets with integer bitmasks and isolate each free column with free & (-free) — the fastest practical solver.

approach-2.ts
function solveNQueens(n: number): string[][] {
  const results: string[][] = [];
  const queens: number[] = [];        // queens[r] = column chosen for row r
  const all = (1 << n) - 1;           // n low bits set = every column open

  // cols, diag, anti are bitmasks of ATTACKED columns for the current row.
  function backtrack(cols: number, diag: number, anti: number): void {
    if (queens.length === n) {
      results.push(
        queens.map((c) => '.'.repeat(c) + 'Q' + '.'.repeat(n - c - 1))
      );
      return;
    }
    // Bits that are 1 here are columns NOT attacked by any axis.
    let free = all & ~(cols | diag | anti);
    while (free !== 0) {
      const bit = free & -free;       // lowest set bit = next candidate column
      free ^= bit;                    // consume it for this loop iteration
      const col = Math.log2(bit) | 0; // index of that bit (0 .. n-1)

      queens.push(col);
      // Shift diag/anti by one: moving to the next row slides both diagonals.
      backtrack(cols | bit, (diag | bit) << 1, (anti | bit) >> 1);
      queens.pop();
    }
  }

  backtrack(0, 0, 0);
  return results;
}
Note → diag shifts left and anti shifts right each row because a diagonal threat moves one column over per row. Bits that slide past the board edge vanish automatically when masked by all. For n ≤ 31 the masks fit in a single 32-bit integer.

ALT 2 Brute force over permutations

Time O(n!·n) · Space O(n)

Any permutation of column indices already places exactly one queen per row and column — so only the two diagonals need checking.

approach-3.ts
function solveNQueens(n: number): string[][] {
  const results: string[][] = [];
  const perm: number[] = [];          // perm[r] = column of queen in row r
  const used: boolean[] = new Array(n).fill(false);

  // A permutation guarantees distinct rows AND columns automatically.
  // Two queens conflict diagonally iff |r1 - r2| === |c1 - c2|.
  function diagonalsOk(row: number, col: number): boolean {
    for (let r = 0; r < row; r++) {
      if (Math.abs(row - r) === Math.abs(col - perm[r])) return false;
    }
    return true;
  }

  function build(row: number): void {
    if (row === n) {
      results.push(
        perm.map((c) => '.'.repeat(c) + 'Q' + '.'.repeat(n - c - 1))
      );
      return;
    }
    for (let col = 0; col < n; col++) {
      if (used[col]) continue;        // enforce the permutation (unique columns)
      if (!diagonalsOk(row, col)) continue;
      used[col] = true;
      perm.push(col);
      build(row + 1);
      perm.pop();
      used[col] = false;
    }
  }

  build(0);
  return results;
}
Note → This frames the search as enumerating column permutations: the used[] flags keep columns distinct, leaving only diagonal validation. The diagonal scan is O(n) per placement, giving the O(n!·n) bound — clearer to reason about, but slower than the O(1) set or bitmask checks.

MNEMONIC The one-liner

"One queen per row — place, mark three sets, recurse, unmark. Row-by-row with O(1) pruning."

TRIGGERS When you see ___ → reach for ___

place items on a grid with mutual exclusion constraintsrow-by-row backtracking + sets
count / enumerate all valid board arrangementsbacktrack with base case at row === n
"no two on the same diagonal"r-c set (main) and r+c set (anti)
need to undo a placement after exploring a subtreeadd before recurse, delete after

SKELETON The reusable shape

skeleton.ts
const cols = new Set<number>();
const diag = new Set<number>(); // r - c
const anti = new Set<number>(); // r + c
const queens: number[] = [];

function backtrack(row: number): void {
  if (row === n) { /* record solution */ return; }
  for (let col = 0; col < n; col++) {
    if (cols.has(col) || diag.has(row - col) || anti.has(row + col)) continue;
    queens.push(col); cols.add(col); diag.add(row - col); anti.add(row + col);
    backtrack(row + 1);
    queens.pop();   cols.delete(col); diag.delete(row - col); anti.delete(row + col);
  }
}
backtrack(0);

FLASHCARDS Tap to flip

What do the three sets track?
cols: occupied columns. diag: values of row - col (main diagonals). anti: values of row + col (anti-diagonals).
tap to flip
1 / 8
Answer to reveal explanations. No penalty for retries.Score: 0 / 9
QUESTION 01
What do we store in the diag set to detect main-diagonal conflicts?
QUESTION 02
Why do we recurse by row (one call per row) rather than trying every cell freely?
QUESTION 03
For n=4, how many valid solutions exist?
QUESTION 04
After backtrack(row + 1) returns, you must:
QUESTION 05
What is the worst-case time complexity?
QUESTION 06
A queen is placed at row 2, col 3. Which values are added to the three sets?
QUESTION 07
Which modification turns N-Queens (return all boards) into N-Queens II (return the count)?
QUESTION 08
#51 · N-QueensPlace one queen per row, tracking used columns and both diagonals (r−c and r+c) in sets. At each row, try every valid column, recurse, then backtrack. Collect board representations at depth n.Which algorithmic approach does this primarily use?
QUESTION 09
#51 · N-QueensPlace one queen per row, tracking used columns and both diagonals (r−c and r+c) in sets. At each row, try every valid column, recurse, then backtrack. Collect board representations at depth n.Which implementation correctly solves it?