Skip to content

51. N-Queens (Hard)

Problem

Given an integer n, return all distinct solutions to the n-queens puzzle. Each solution is a placement of n queens on an n × n board such that no two queens share a row, column, or diagonal.

Each solution should be a list of strings where "Q" is a queen and "." is empty.

Example

  • n = 4[[".Q..","...Q","Q...","..Q."], ["..Q.","Q...","...Q",".Q.."]]
  • n = 1[["Q"]]

LeetCode 51 · Link · Hard

Approach 1: Brute force, try all n^n placements

Try every possible column for every row; filter valid placements.

from itertools import product
def solve_n_queens(n):
result = []
for cols in product(range(n), repeat=n): # L1: O(n^n) outer enumeration
if len(set(cols)) != n:
continue # L2: column conflict
ok = True
for r1 in range(n):
for r2 in range(r1 + 1, n):
if abs(cols[r1] - cols[r2]) == r2 - r1:
ok = False; break # L3: O(n²) diagonal check
if not ok: break
if ok:
board = [["." if c != cols[r] else "Q" for c in range(n)] for r in range(n)]
result.append(["".join(row) for row in board])
return result

Complexity

  • Time: O(n^n · n²). Infeasible past n ≈ 7.
  • Space: same.

Approach 2: Backtracking row-by-row with linear conflict check

Place one queen per row; for each row, try each column, check conflicts by walking previously placed queens.

def solve_n_queens(n):
result = []
cols = [-1] * n
def valid(r, c):
for r2 in range(r):
if cols[r2] == c or abs(cols[r2] - c) == r - r2: # L1: O(r) check
return False
return True
def backtrack(r):
if r == n:
result.append(["".join("Q" if cols[i] == j else "." for j in range(n)) for i in range(n)])
return
for c in range(n):
if valid(r, c): # L2: O(r) per column
cols[r] = c
backtrack(r + 1) # L3: recurse
cols[r] = -1
backtrack(0)
return result

Complexity

  • Time: exponential; conflict check per placement is O(r).
  • Space: O(n) recursion + output.

Approach 3: Backtracking with column + diagonal sets (optimal)

Maintain three sets: used columns, used row + col diagonals (anti-diagonals), used row - col diagonals. All checks are O(1).

def solve_n_queens(n):
result = []
cols_used = set()
diag1 = set() # row + col
diag2 = set() # row - col
placement = [-1] * n
def backtrack(r):
if r == n:
board = ["".join("Q" if placement[i] == j else "." for j in range(n)) for i in range(n)]
result.append(board) # L1: O(n²) build board
return
for c in range(n):
if c in cols_used or (r + c) in diag1 or (r - c) in diag2:
continue # L2: O(1) conflict check
cols_used.add(c) # L3: O(1) mark column
diag1.add(r + c) # L4: O(1) mark anti-diag
diag2.add(r - c) # L5: O(1) mark main diag
placement[r] = c
backtrack(r + 1) # L6: recurse to next row
cols_used.remove(c) # L7: O(1) unmark
diag1.remove(r + c)
diag2.remove(r - c)
backtrack(0)
return result

Where the time goes, line by line

Variables: n = the board size.

LinePer-call costTimes executedContribution
L1 (build board)O(n²)solutionsO(solutions · n²)
L2 (conflict check)O(1)n per row per pathO(n · n!)
L3/L4/L5 (mark sets)O(1)valid placementsO(n!)
L6 (recurse)O(1) dispatchO(n!) nodesO(n!) ← dominates
L7 (unmark)O(1)valid placementsO(n!)

The recursion tree has at most n! leaves (after column pruning). Each level has at most n candidates, but the conflict sets reduce the effective branching to roughly n - row.

Complexity

  • Time: O(n!). Roughly n × (n-2) × (n-4) × … branches after pruning.
  • Space: O(n) sets + O(n) recursion.

Why row + col and row - col?

Cells on the same anti-diagonal share row + col (constant along the up-right direction). Cells on the same main diagonal share row - col (constant along the down-right direction). Two integers per conflict dimension suffice to make every check O(1).

Summary

ApproachTimeSpace
Brute enumerate placementsO(n^n · n²)O(n^n)
Row-by-row + linear checkO(n!) · O(n)O(n)
Row-by-row + conflict setsO(n!)O(n)

The conflict-set template is the classic N-Queens solution and the template for constraint-satisfaction problems more broadly (Sudoku solver, exact cover).

Test cases

def solve_n_queens(n):
result = []
cols_used = set()
diag1 = set()
diag2 = set()
placement = [-1] * n
def backtrack(r):
if r == n:
board = ["".join("Q" if placement[i] == j else "." for j in range(n)) for i in range(n)]
result.append(board)
return
for c in range(n):
if c in cols_used or (r + c) in diag1 or (r - c) in diag2:
continue
cols_used.add(c); diag1.add(r + c); diag2.add(r - c)
placement[r] = c
backtrack(r + 1)
cols_used.remove(c); diag1.remove(r + c); diag2.remove(r - c)
backtrack(0)
return result
def _run_tests():
# n=1: one solution
assert solve_n_queens(1) == [["Q"]]
# n=4: two solutions
r4 = solve_n_queens(4)
assert len(r4) == 2
assert sorted(r4) == sorted([[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]])
# n=5: 10 solutions
assert len(solve_n_queens(5)) == 10
# no solution for n=2 or n=3
assert solve_n_queens(2) == []
assert solve_n_queens(3) == []
print("all tests pass")
if __name__ == "__main__":
_run_tests()