52. N-Queens II
Problem Description
The n-queens puzzle asks you to place n
queens on an n x n
chessboard such that no two queens can attack each other.
In chess, a queen can attack any piece that is:
- In the same row
- In the same column
- On the same diagonal (both positive and negative diagonals)
Given an integer n
representing the size of the chessboard, you need to find and return the total number of distinct valid arrangements where all n
queens can be placed on the board without any queen attacking another.
For example, if n = 4
, there are 2 distinct solutions to place 4 queens on a 4x4 board where no queens attack each other. The function should return 2
in this case.
The challenge is to explore all possible placements systematically while ensuring that each placement follows the constraint that no two queens can attack each other.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves placing queens on a chessboard, not traversing nodes and edges in a graph structure.
Need to solve for kth smallest/largest?
- No: We're counting the total number of valid solutions, not finding a kth smallest or largest element.
Involves Linked Lists?
- No: The problem deals with a 2D chessboard representation, not linked list operations.
Does the problem have small constraints?
- Yes: The n-queens problem typically has small constraints (n is usually ≤ 15 or so) because the solution space grows exponentially. We need to explore all possible valid placements of n queens on an n×n board.
Brute force / Backtracking?
- Yes: We need to systematically explore all possible queen placements while maintaining constraints (no two queens attacking each other). When we find an invalid placement, we backtrack and try different positions. This is a classic backtracking scenario where we:
- Place a queen in a valid position
- Move to the next row
- If we can't place a queen in the next row, backtrack to the previous row
- Try a different column position
- Continue until we've explored all possibilities
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This matches perfectly with the solution, which uses DFS with backtracking to explore all valid queen placements row by row, maintaining arrays to track occupied columns and diagonals.
Intuition
When we think about placing queens on a chessboard, we need to ensure no two queens can attack each other. Since queens attack along rows, columns, and diagonals, we need a systematic way to explore valid placements.
The key insight is that we must place exactly one queen per row (since n queens on n rows means one queen per row). This immediately simplifies our problem - instead of considering all possible positions on the board, we only need to decide which column to place the queen in for each row.
We can build our solution row by row. Starting from the first row, we try placing a queen in each column. For each placement, we check if it conflicts with previously placed queens. If it's valid, we move to the next row and repeat. If we successfully place queens in all n rows, we've found one valid solution.
The challenge is tracking conflicts efficiently. A queen at position (i, j)
attacks:
- All positions in column
j
- All positions on the diagonal where
row + col = i + j
(positive diagonal) - All positions on the diagonal where
row - col = i - j
(negative diagonal)
We can use three boolean arrays to track:
cols[j]
: whether columnj
is occupieddg[i+j]
: whether the positive diagonal passing through(i,j)
is occupiedudg[i-j+n]
: whether the negative diagonal passing through(i,j)
is occupied (we addn
to avoid negative indices)
When we can't place a queen in any column of the current row (all positions conflict), we backtrack to the previous row, remove the queen we placed there, and try the next column. This backtracking ensures we explore all possible valid configurations systematically.
The beauty of this approach is that we're essentially building a decision tree where each level represents a row, and each branch represents choosing a column for that row's queen. We traverse this tree depth-first, counting each path that reaches the bottom (all n queens successfully placed).
Learn more about Backtracking patterns.
Solution Approach
The implementation uses a depth-first search (DFS) with backtracking to systematically explore all possible queen placements.
Core Algorithm Structure:
The solution defines a recursive function dfs(i)
that attempts to place queens starting from row i
. The base case occurs when i == n
, meaning we've successfully placed queens in all n rows, so we increment our answer counter.
Data Structures:
Three boolean arrays track the attack zones:
cols[j]
: Tracks if columnj
has a queendg[a]
: Tracks if diagonal with suma = row + col
has a queenudg[b]
: Tracks if anti-diagonal with differenceb = row - col + n
has a queen
The arrays are sized to handle all possible diagonal indices:
cols
needs sizen
(but allocated as 10 for simplicity)dg
needs size up to2n-1
(allocated as 20)udg
needs size up to2n-1
(allocated as 20)
Step-by-Step Process:
-
Start with
dfs(0)
to begin placing queens from row 0. -
For the current row
i
, iterate through each columnj
from 0 to n-1. -
For each position
(i, j)
, calculate:- Diagonal index:
a = i + j
- Anti-diagonal index:
b = i - j + n
(addingn
to ensure non-negative index)
- Diagonal index:
-
Check if placing a queen at
(i, j)
causes a conflict:- If
cols[j]
ordg[a]
orudg[b]
is True, skip this column
- If
-
If no conflict, place the queen:
- Mark
cols[j] = dg[a] = udg[b] = True
- Recursively call
dfs(i + 1)
to place queens in the next row
- Mark
-
After the recursive call returns (exploring all possibilities with this placement), backtrack:
- Reset
cols[j] = dg[a] = udg[b] = False
- Continue to try the next column
- Reset
-
The variable
ans
accumulates the count each time we successfully place all n queens (reach the base case).
Why This Works:
The algorithm ensures we explore every valid configuration exactly once. By processing row by row and using the conflict-checking arrays, we avoid generating invalid configurations. The backtracking mechanism ensures we don't miss any valid solutions - when we hit a dead end, we systematically undo our choices and try alternatives.
The time complexity is O(n!), as in the worst case we might explore n choices for the first row, n-1 for the second, and so on. The space complexity is O(n) for the recursion stack and the tracking arrays.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through finding all solutions for n=4 (a 4x4 chessboard).
Initial Setup:
- We have arrays:
cols[4]
,dg[7]
,udg[7]
all initialized to False - Start with
dfs(0)
to place queens from row 0 - Our answer counter
ans = 0
Step 1: Row 0, Try Column 0
- Position (0,0): diagonal = 0+0 = 0, anti-diagonal = 0-0+4 = 4
- No conflicts (all arrays are False)
- Set
cols[0] = dg[0] = udg[4] = True
- Call
dfs(1)
Step 2: Row 1, Try Columns
- Column 0: (1,0) → diagonal=1, anti-diagonal=5
cols[0]
is True → CONFLICT, skip
- Column 1: (1,1) → diagonal=2, anti-diagonal=4
udg[4]
is True → CONFLICT, skip
- Column 2: (1,2) → diagonal=3, anti-diagonal=3
- No conflicts! Set
cols[2] = dg[3] = udg[3] = True
- Call
dfs(2)
- No conflicts! Set
Step 3: Row 2, Try Columns
- Column 0:
cols[0]
is True → CONFLICT - Column 1: (2,1) → diagonal=3, anti-diagonal=5
dg[3]
is True → CONFLICT
- Column 2:
cols[2]
is True → CONFLICT - Column 3: (2,3) → diagonal=5, anti-diagonal=3
udg[3]
is True → CONFLICT
- All columns conflict! Backtrack to Row 1
Step 4: Backtrack to Row 1, Continue
- Undo:
cols[2] = dg[3] = udg[3] = False
- Column 3: (1,3) → diagonal=4, anti-diagonal=2
- No conflicts! Set
cols[3] = dg[4] = udg[2] = True
- Call
dfs(2)
- No conflicts! Set
Step 5: Row 2, With New Configuration
- Column 0:
cols[0]
is True → CONFLICT - Column 1: (2,1) → diagonal=3, anti-diagonal=5
- No conflicts! Set
cols[1] = dg[3] = udg[5] = True
- Call
dfs(3)
- No conflicts! Set
Step 6: Row 3, Try to Complete
- Column 0:
cols[0]
is True → CONFLICT - Column 1:
cols[1]
is True → CONFLICT - Column 2: (3,2) → diagonal=5, anti-diagonal=5
udg[5]
is True → CONFLICT
- Column 3:
cols[3]
is True → CONFLICT - Dead end! Backtrack
Continuing the Process: The algorithm continues backtracking and trying new combinations. Eventually, it finds the first valid solution when queens are placed at columns [1,3,0,2] for rows [0,1,2,3]:
. Q . . . . . Q Q . . . . . Q .
When dfs(4)
is reached (all 4 queens placed), ans
increments to 1.
The algorithm then backtracks completely and continues exploring. It finds the second valid solution with queens at columns [2,0,3,1]:
. . Q . Q . . . . . . Q . Q . .
After exploring all possibilities from row 0, the algorithm returns ans = 2
.
Key Observations:
- The diagonal tracking prevents conflicts efficiently: when Queen at (0,0) is placed, any position with diagonal sum 0 or anti-diagonal difference 4 is immediately marked as conflicting
- Backtracking ensures we explore all branches: when we hit a dead end in row 2, we return to row 1 and try the next option
- The row-by-row approach guarantees exactly one queen per row, simplifying our search space
Solution Implementation
1class Solution:
2 def totalNQueens(self, n: int) -> int:
3 def backtrack(row: int) -> None:
4 # Base case: all queens have been successfully placed
5 if row == n:
6 nonlocal solution_count
7 solution_count += 1
8 return
9
10 # Try placing a queen in each column of the current row
11 for col in range(n):
12 # Calculate diagonal and anti-diagonal indices
13 # Diagonal: row + col (constant along each diagonal)
14 # Anti-diagonal: row - col + n (add n to ensure positive index)
15 diagonal_idx = row + col
16 anti_diagonal_idx = row - col + n
17
18 # Check if current position conflicts with existing queens
19 if columns_used[col] or diagonals_used[diagonal_idx] or anti_diagonals_used[anti_diagonal_idx]:
20 continue
21
22 # Place the queen by marking column and diagonals as used
23 columns_used[col] = True
24 diagonals_used[diagonal_idx] = True
25 anti_diagonals_used[anti_diagonal_idx] = True
26
27 # Recursively place queens in the next row
28 backtrack(row + 1)
29
30 # Backtrack: remove the queen and unmark the positions
31 columns_used[col] = False
32 diagonals_used[diagonal_idx] = False
33 anti_diagonals_used[anti_diagonal_idx] = False
34
35 # Initialize tracking arrays for column and diagonal conflicts
36 columns_used = [False] * n # Track which columns have queens
37 diagonals_used = [False] * (2 * n) # Track main diagonals (row + col)
38 anti_diagonals_used = [False] * (2 * n) # Track anti-diagonals (row - col + n)
39
40 # Counter for total number of valid solutions
41 solution_count = 0
42
43 # Start the backtracking from the first row
44 backtrack(0)
45
46 return solution_count
47
1class Solution {
2 // Board size
3 private int n;
4 // Counter for valid solutions
5 private int ans;
6 // Track which columns are occupied by queens
7 private boolean[] cols = new boolean[10];
8 // Track which main diagonals (top-left to bottom-right) are occupied
9 // For cells on the same main diagonal: row + col is constant
10 private boolean[] dg = new boolean[20];
11 // Track which anti-diagonals (top-right to bottom-left) are occupied
12 // For cells on the same anti-diagonal: row - col is constant
13 // Add n to make indices non-negative
14 private boolean[] udg = new boolean[20];
15
16 /**
17 * Calculate the total number of distinct solutions to the n-queens puzzle
18 * @param n The size of the chessboard (n x n)
19 * @return The number of distinct solutions
20 */
21 public int totalNQueens(int n) {
22 this.n = n;
23 dfs(0);
24 return ans;
25 }
26
27 /**
28 * Depth-first search to place queens row by row
29 * @param row Current row index being processed
30 */
31 private void dfs(int row) {
32 // Base case: all queens successfully placed
33 if (row == n) {
34 ++ans;
35 return;
36 }
37
38 // Try placing a queen in each column of the current row
39 for (int col = 0; col < n; ++col) {
40 // Calculate diagonal indices
41 int mainDiagonal = row + col;
42 int antiDiagonal = row - col + n;
43
44 // Check if current position conflicts with existing queens
45 if (cols[col] || dg[mainDiagonal] || udg[antiDiagonal]) {
46 continue;
47 }
48
49 // Place queen at (row, col)
50 cols[col] = true;
51 dg[mainDiagonal] = true;
52 udg[antiDiagonal] = true;
53
54 // Recursively place queens in the next row
55 dfs(row + 1);
56
57 // Backtrack: remove queen from (row, col)
58 cols[col] = false;
59 dg[mainDiagonal] = false;
60 udg[antiDiagonal] = false;
61 }
62 }
63}
64
1class Solution {
2public:
3 int totalNQueens(int n) {
4 // Use bitsets to track occupied columns and diagonals
5 // cols[j] = 1 means column j is occupied by a queen
6 bitset<10> cols;
7 // dg[k] = 1 means diagonal with sum index k is occupied
8 // For diagonal: row + col = constant
9 bitset<20> diagonals;
10 // udg[k] = 1 means anti-diagonal with difference index k is occupied
11 // For anti-diagonal: row - col = constant (shifted by n to avoid negative indices)
12 bitset<20> antiDiagonals;
13
14 int count = 0; // Total number of valid N-Queens solutions
15
16 // Recursive function to place queens row by row
17 function<void(int)> placeQueens = [&](int row) {
18 // Base case: all queens successfully placed
19 if (row == n) {
20 ++count;
21 return;
22 }
23
24 // Try placing a queen in each column of the current row
25 for (int col = 0; col < n; ++col) {
26 // Calculate diagonal and anti-diagonal indices
27 int diagonalIdx = row + col;
28 int antiDiagonalIdx = row - col + n; // Add n to handle negative values
29
30 // Check if current position conflicts with existing queens
31 if (cols[col] || diagonals[diagonalIdx] || antiDiagonals[antiDiagonalIdx]) {
32 continue;
33 }
34
35 // Place the queen and mark the column and diagonals as occupied
36 cols[col] = true;
37 diagonals[diagonalIdx] = true;
38 antiDiagonals[antiDiagonalIdx] = true;
39
40 // Recursively place queens in the next row
41 placeQueens(row + 1);
42
43 // Backtrack: remove the queen and unmark the positions
44 cols[col] = false;
45 diagonals[diagonalIdx] = false;
46 antiDiagonals[antiDiagonalIdx] = false;
47 }
48 };
49
50 // Start placing queens from the first row
51 placeQueens(0);
52
53 return count;
54 }
55};
56
1/**
2 * Solves the N-Queens problem and returns the number of distinct solutions
3 * @param n - The size of the chessboard (n x n) and number of queens to place
4 * @returns The total number of valid N-Queens solutions
5 */
6function totalNQueens(n: number): number {
7 // Track which columns are occupied by queens
8 const columnsOccupied: boolean[] = Array(10).fill(false);
9
10 // Track which main diagonals (top-left to bottom-right) are occupied
11 // Main diagonals have constant row + column sum
12 const mainDiagonals: boolean[] = Array(20).fill(false);
13
14 // Track which anti-diagonals (top-right to bottom-left) are occupied
15 // Anti-diagonals have constant row - column difference
16 const antiDiagonals: boolean[] = Array(20).fill(false);
17
18 // Counter for valid solutions
19 let solutionCount: number = 0;
20
21 /**
22 * Depth-first search to place queens row by row
23 * @param currentRow - The current row index where we're trying to place a queen
24 */
25 const placeQueens = (currentRow: number): void => {
26 // Base case: all queens successfully placed
27 if (currentRow === n) {
28 solutionCount++;
29 return;
30 }
31
32 // Try placing a queen in each column of the current row
33 for (let column: number = 0; column < n; column++) {
34 // Calculate diagonal indices
35 const mainDiagonalIndex: number = currentRow + column;
36 const antiDiagonalIndex: number = currentRow - column + n; // Add n to ensure positive index
37
38 // Check if current position conflicts with existing queens
39 if (columnsOccupied[column] ||
40 mainDiagonals[mainDiagonalIndex] ||
41 antiDiagonals[antiDiagonalIndex]) {
42 continue;
43 }
44
45 // Place queen at current position
46 columnsOccupied[column] = true;
47 mainDiagonals[mainDiagonalIndex] = true;
48 antiDiagonals[antiDiagonalIndex] = true;
49
50 // Recursively place queens in the next row
51 placeQueens(currentRow + 1);
52
53 // Backtrack: remove queen from current position
54 columnsOccupied[column] = false;
55 mainDiagonals[mainDiagonalIndex] = false;
56 antiDiagonals[antiDiagonalIndex] = false;
57 }
58 };
59
60 // Start placing queens from the first row
61 placeQueens(0);
62
63 return solutionCount;
64}
65
Time and Space Complexity
Time Complexity: O(n!)
The algorithm explores all possible placements of n queens on an n×n board. At the first row, we have n
choices for placing a queen. Due to the constraints (no two queens can attack each other), the number of valid positions decreases as we place more queens. In the worst case, we have approximately n × (n-2) × (n-4) × ...
possibilities, which gives us a time complexity of O(n!)
. The backtracking with pruning (checking columns and diagonals) helps eliminate invalid branches early, but the worst-case complexity remains factorial.
Space Complexity: O(n)
The space complexity consists of:
- The recursion call stack depth, which goes up to
n
levels (one for each row):O(n)
- The
cols
array of size 10:O(1)
since it's a fixed size regardless of input - The
dg
(diagonal) array of size 20:O(1)
since it's a fixed size - The
udg
(anti-diagonal) array of size 20:O(1)
since it's a fixed size - Other variables (
ans
,i
,j
,a
,b
):O(1)
Note that while the arrays are fixed-size in this implementation (10, 20, 20), they should ideally be sized based on n
. For a proper implementation, cols
should be size n
, and both diagonal arrays should be size 2n-1
. Even with proper sizing, the space complexity would be O(n) + O(n) + O(n) = O(n)
.
The dominant factor is the recursion stack depth of O(n)
, giving us an overall space complexity of O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Diagonal Index Calculation
The Pitfall:
A frequent mistake is incorrectly calculating the diagonal indices, particularly for the anti-diagonal. Developers often use row - col
directly without adjusting for negative indices, which causes array index out of bounds errors.
# WRONG - can produce negative indices anti_diagonal_idx = row - col # When row=0, col=3, this gives -3!
The Solution: Always add an offset to ensure non-negative indices for anti-diagonals:
# CORRECT - ensures all indices are non-negative anti_diagonal_idx = row - col + n - 1 # or row - col + n
2. Forgetting to Backtrack (Reset State)
The Pitfall: After exploring a branch in the recursion tree, forgetting to reset the state arrays leads to incorrect results. The algorithm will think positions are occupied when they're not, missing valid solutions.
# WRONG - missing backtrack step columns_used[col] = True diagonals_used[diagonal_idx] = True anti_diagonals_used[anti_diagonal_idx] = True backtrack(row + 1) # Missing reset here!
The Solution: Always reset the state after the recursive call returns:
# CORRECT - proper backtracking columns_used[col] = True diagonals_used[diagonal_idx] = True anti_diagonals_used[anti_diagonal_idx] = True backtrack(row + 1) # Reset state for next iteration columns_used[col] = False diagonals_used[diagonal_idx] = False anti_diagonals_used[anti_diagonal_idx] = False
3. Incorrect Array Size for Diagonal Tracking
The Pitfall:
Using insufficient array sizes for diagonal tracking can cause index out of bounds errors. For an n×n board, the diagonal index can range up to 2n-2
.
# WRONG - arrays too small diagonals_used = [False] * n # Not enough space! anti_diagonals_used = [False] * n # Not enough space!
The Solution:
Allocate arrays with size at least 2n-1
to handle all possible diagonal indices:
# CORRECT - proper array sizing diagonals_used = [False] * (2 * n) # Safe size with slight overhead anti_diagonals_used = [False] * (2 * n) # Safe size with slight overhead
4. Using Global Variables Without Proper Scoping
The Pitfall: Using a regular variable for the solution count inside the nested function won't work due to Python's scoping rules:
# WRONG - UnboundLocalError
def totalNQueens(self, n: int) -> int:
def backtrack(row: int):
if row == n:
solution_count += 1 # Error: local variable referenced before assignment
The Solution:
Use the nonlocal
keyword to modify the outer scope variable:
# CORRECT - using nonlocal
def totalNQueens(self, n: int) -> int:
def backtrack(row: int):
if row == n:
nonlocal solution_count
solution_count += 1
Alternatively, use a mutable container like a list:
# ALTERNATIVE - using a list
def totalNQueens(self, n: int) -> int:
solution_count = [0] # Use list to avoid nonlocal
def backtrack(row: int):
if row == n:
solution_count[0] += 1
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!