N-Queens II
HardPrompt
The n-queens puzzle is the problem of placing n queens on an n x n chessboard such that no two queens attack each other.
Given an integer n, return the number of distinct solutions to the n-queens puzzle.
Example 1:
Input: n = 4
Output: 2
Explanation: There are two distinct solutions to the 4-queens puzzle as shown.Example 2:
Input: n = 1
Output: 1
Constraints:
1 <= n <= 9
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a standard backtracking algorithm to explore all possible placements of queens. We build the solution row by row, ensuring that each new queen placed does not attack any previously placed queens. To efficiently check for attacks, we use helper boolean arrays to keep track of which columns and diagonals are already occupied.
Algorithm
- Initialize a global or member variable
countto store the number of solutions. - Create three boolean arrays to keep track of occupied columns and diagonals:
colsof sizen.diag1of size2*n - 1for main diagonals (row - col).diag2of size2*n - 1for anti-diagonals (row + col).
- Implement a recursive backtracking function, say
backtrack(row). - Base Case: If
row == n, it means we have successfully placednqueens. Incrementcountand return. - Recursive Step: Iterate through each column
colfrom0ton-1for the currentrow.- Calculate the indices for the two diagonals:
d1 = row - col + n - 1andd2 = row + col. - Check if the current position
(row, col)is safe by checking ifcols[col],diag1[d1], anddiag2[d2]are allfalse. - If the position is safe:
- Place Queen: Mark
cols[col],diag1[d1], anddiag2[d2]astrue. - Recurse: Call
backtrack(row + 1)to solve for the next row. - Backtrack: Un-place the queen by resetting
cols[col],diag1[d1], anddiag2[d2]tofalse. This allows exploration of other possibilities.
- Place Queen: Mark
- Calculate the indices for the two diagonals:
- Start the process by calling
backtrack(0). - Return the final
count.
Walkthrough
The core idea is to place one queen in each row, starting from row 0. For each row, we iterate through all columns to find a safe square. A square (row, col) is considered safe if no other queen (in rows 0 to row-1) is in the same column, same main diagonal, or same anti-diagonal.
We use three boolean arrays to maintain the state:
cols[c]istrueif a queen is already placed in columnc.diag1[r-c+n-1]istrueif a queen is on the main diagonal identified byr-c. We addn-1to map the result (which can be negative) to a valid array index.diag2[r+c]istrueif a queen is on the anti-diagonal identified byr+c.
When the recursive function backtrack(row) is called, it tries to place a queen in each column of that row. If a safe column is found, it marks the corresponding column and diagonals as occupied, and then makes a recursive call for the next row, backtrack(row + 1). After the recursive call returns, it backtracks by un-marking the column and diagonals, allowing the exploration of placing the queen in other columns of the current row. If the recursion reaches row == n, a valid solution has been found, and we increment a counter.
class Solution { private int count = 0; private boolean[] cols; private boolean[] diag1; private boolean[] diag2; private int n; public int totalNQueens(int n) { this.n = n; this.cols = new boolean[n]; this.diag1 = new boolean[2 * n - 1]; this.diag2 = new boolean[2 * n - 1]; backtrack(0); return count; } private void backtrack(int row) { if (row == n) { count++; return; } for (int col = 0; col < n; col++) { int d1_idx = row - col + n - 1; int d2_idx = row + col; if (cols[col] || diag1[d1_idx] || diag2[d2_idx]) { continue; } // Place the queen cols[col] = true; diag1[d1_idx] = true; diag2[d2_idx] = true; // Recurse for the next row backtrack(row + 1); // Backtrack (remove the queen) cols[col] = false; diag1[d1_idx] = false; diag2[d2_idx] = false; } }}Complexity
Time
O(N!)
Space
O(N)
Trade-offs
Pros
The logic is straightforward and closely follows the definition of the N-Queens problem.
It's a general backtracking template that can be adapted to similar constraint satisfaction problems.
Cons
Slightly less performant than the bitmasking approach due to the overhead of array indexing and assignments compared to fast bitwise operations.
Uses more memory to store the boolean arrays compared to a few integers.
This approach refines the standard backtracking algorithm by using bitmasks (integers) instead of boolean arrays to track the state of the board. Each bit in an integer can represent a column or a diagonal, allowing for extremely fast state updates and checks using bitwise operations. This significantly improves the performance of the algorithm, even though the time complexity remains the same in Big-O terms.
Algorithm
- Define a recursive function, say
solve(row, cols, diag1, diag2), where the integer parameters are bitmasks representing the occupied columns and diagonals. - Base Case: If
row == n, a valid placement has been found. Return 1. - Recursive Step:
- Calculate a bitmask of all available positions in the current row. The mask for all attacked columns is
(cols | diag1 | diag2). The mask for available columns is((1 << n) - 1) & ~(cols | diag1 | diag2). - Initialize a local counter for solutions found from this state:
solutions = 0. - Loop as long as there are available positions in the
availablePositionsmask. - In each iteration, pick one available position. A common trick is to isolate the least significant bit (rightmost '1'):
position = availablePositions & -availablePositions. - Remove this position from the set of available positions for the current loop:
availablePositions -= position. - Make a recursive call for the next row with updated masks and add the result to
solutions:
- New column mask:
cols | position - New main diagonal mask:
(diag1 | position) << 1(shifted left for the next row) - New anti-diagonal mask:
(diag2 | position) >> 1(shifted right for the next row) solutions += solve(row + 1, new_cols, new_diag1, new_diag2).
- Return the total
solutionsfound.
- Calculate a bitmask of all available positions in the current row. The mask for all attacked columns is
- The initial call to start the process is
solve(0, 0, 0, 0).
Walkthrough
The fundamental backtracking structure is the same, but the implementation of state tracking is optimized. We use three integers as bitmasks:
cols: A bitmask where thei-th bit is 1 if columniis occupied.diag1: A bitmask for the main diagonals (row - col).diag2: A bitmask for the anti-diagonals (row + col).
In the recursive function solve(row, cols, diag1, diag2), we first compute a bitmask of all safe columns for the current row. The combined mask of all attacked columns is cols | diag1 | diag2. We can find the safe columns by inverting this mask and ensuring we only consider n bits: availablePositions = ((1 << n) - 1) & ~(cols | diag1 | diag2).
We then iterate through each set bit in availablePositions. For each safe column (represented by a bit position), we make a recursive call for the next row. The key insight is how the diagonal masks are updated for the next row:
- The main diagonal (
row - col) values increase by 1 for each column as we move to the next row, which corresponds to a left shift (<< 1) of the bitmask. - The anti-diagonal (
row + col) values also change consistently, which corresponds to a right shift (>> 1) of the bitmask.
This method avoids array lookups and replaces them with highly efficient bitwise operations, resulting in a much faster solution.
class Solution { private int size; public int totalNQueens(int n) { this.size = n; return solve(0, 0, 0, 0); } private int solve(int row, int cols, int diag1, int diag2) { // Base case: If all rows are filled, we found one solution. if (row == size) { return 1; } int count = 0; // Calculate available positions in the current row. // ((1 << size) - 1) creates a mask with 'size' number of 1s. int availablePositions = ((1 << size) - 1) & (~(cols | diag1 | diag2)); // Iterate through all available positions. while (availablePositions != 0) { // Get the least significant bit (the rightmost available column). int position = availablePositions & -availablePositions; // Remove this position from the available set for the next iteration. availablePositions -= position; // Recurse for the next row with updated masks. count += solve(row + 1, cols | position, (diag1 | position) << 1, (diag2 | position) >> 1); } return count; }}Complexity
Time
O(N!)
Space
O(N)
Trade-offs
Pros
Extremely fast and efficient due to the use of bitwise operations.
Very low memory usage, as it only requires a few integer variables for state tracking.
Cons
The logic, especially the shifting of diagonal masks, can be less intuitive and harder to debug than the array-based approach.
Requires familiarity with bit manipulation techniques.
Solutions
Solution
public class Solution { public int TotalNQueens(int n) { bool[] cols = new bool[10]; bool[] dg = new bool[20]; bool[] udg = new bool[20]; int ans = 0; void dfs(int i) { if (i == n) { ans++; return; } for (int j = 0; j < n; j++) { int a = i + j, b = i - j + n; if (cols[j] || dg[a] || udg[b]) { continue; } cols[j] = dg[a] = udg[b] = true; dfs(i + 1); cols[j] = dg[a] = udg[b] = false; } } dfs(0); return ans; }}class Solution {private int n;private int ans;private boolean[] cols = new boolean[10];private boolean[] dg = new boolean[20];private boolean[] udg = new boolean[20];public int totalNQueens(int n) { this.n = n; dfs(0); return ans; }private void dfs(int i) { if (i == n) { ++ans; return; } for (int j = 0; j < n; ++j) { int a = i + j, b = i - j + n; if (cols[j] || dg[a] || udg[b]) { continue; } cols[j] = true; dg[a] = true; udg[b] = true; dfs(i + 1); cols[j] = false; dg[a] = false; udg[b] = false; } }}function totalNQueens ( n ) { const cols = Array ( 10 ). fill ( false ); const dg = Array ( 20 ). fill ( false ); const udg = Array ( 20 ). fill ( false ); let ans = 0 ; const dfs = i => { if ( i === n ) { ++ ans ; return ; } for ( let j = 0 ; j < n ; ++ j ) { let [ a , b ] = [ i + j , i - j + n ]; if ( cols [ j ] || dg [ a ] || udg [ b ]) { continue ; } cols [ j ] = dg [ a ] = udg [ b ] = true ; dfs ( i + 1 ); cols [ j ] = dg [ a ] = udg [ b ] = false ; } }; dfs ( 0 ); return ans ; }class Solution {public: int totalNQueens(int n) { bitset<10> cols; bitset<20> dg; bitset<20> udg; int ans = 0; function<void(int)> dfs = [&](int i) { if (i == n) { ++ans; return; } for (int j = 0; j < n; ++j) { int a = i + j, b = i - j + n; if (cols[j] || dg[a] || udg[b]) continue; cols[j] = dg[a] = udg[b] = 1; dfs(i + 1); cols[j] = dg[a] = udg[b] = 0; } }; dfs(0); return ans; }};class Solution: def totalNQueens(self, n: int) -> int: def dfs(i: int): if i == n: nonlocal ans ans += 1 return for j in range(n): a, b = i + j, i - j + n if cols[j] or dg[a] or udg[b]: continue cols[j] = dg[a] = udg[b] = True dfs(i + 1) cols[j] = dg[a] = udg[b] = False cols = [False] * 10 dg = [False] * 20 udg = [False] * 20 ans = 0 dfs(0) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.