N Queens Problem in DSA Go - Time & Space Complexity
We want to understand how the time needed to solve the N Queens problem grows as the board size increases.
Specifically, how does the number of steps change when we try to place queens on larger boards?
Analyze the time complexity of the following code snippet.
func solveNQueens(n int) [][]string {
var res [][]string
board := make([][]byte, n)
for i := range board {
board[i] = make([]byte, n)
for j := range board[i] {
board[i][j] = '.'
}
}
backtrack(0, board, &res)
return res
}
func backtrack(row int, board [][]byte, res *[][]string) {
n := len(board)
if row == n {
var copyBoard []string
for _, r := range board {
copyBoard = append(copyBoard, string(r))
}
*res = append(*res, copyBoard)
return
}
for col := 0; col < n; col++ {
if isValid(board, row, col) {
board[row][col] = 'Q'
backtrack(row+1, board, res)
board[row][col] = '.'
}
}
}
This code tries to place queens row by row on an n x n chessboard, using backtracking to find all valid solutions.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: The recursive backtracking tries placing a queen in each column of the current row.
- How many times: For each row, it tries up to n columns, and this repeats recursively for all rows.
As the board size n grows, the number of ways to place queens grows very fast because each row tries n columns, and this repeats for n rows.
| Input Size (n) | Approx. Operations |
|---|---|
| 4 | ~200 |
| 8 | ~10,000 |
| 12 | ~1,000,000+ |
Pattern observation: The operations grow faster than just multiplying by n each time; it grows roughly like n factorial because of the many recursive calls and checks.
Time Complexity: O(n!)
This means the time needed grows very fast, roughly like the number of ways to arrange n queens on the board without attacking each other.
[X] Wrong: "The time complexity is just O(n^2) because there are two loops for rows and columns."
[OK] Correct: The code uses recursion and backtracking, which multiplies the work exponentially, not just nested loops. Each row depends on previous rows, causing factorial growth.
Understanding this problem's time complexity helps you explain why some problems need smart pruning or heuristics to solve efficiently, a useful skill in interviews and real projects.
"What if we added memoization to remember invalid board states? How would the time complexity change?"
Practice
N Queens Problem?Solution
Step 1: Understand the problem statement
The N Queens Problem requires placing N queens on an NxN board so that no two queens threaten each other.Step 2: Identify the correct goal
This means no two queens share the same row, column, or diagonal.Final Answer:
Place N queens on an NxN chessboard so that no two queens attack each other -> Option AQuick Check:
Goal = Place queens safely [OK]
- Thinking queens can be in the same row or column
- Confusing the problem with counting queens
- Ignoring diagonal attacks
Solution
Step 1: Understand typical parameters
Usually, the board is represented as a 1D slice of ints where index is row and value is column, and row is int.Step 2: Match correct signature
func solveNQueens(board []int, row int) bool uses a 1D slice and int row, returning bool to indicate success, which is common in backtracking.Final Answer:
func solveNQueens(board []int, row int) bool -> Option AQuick Check:
Correct signature uses []int and int row [OK]
- Using 2D slice which is less efficient
- Using wrong parameter types like string for row
- Returning wrong type instead of bool
func main() {
solutions := solveNQueens(4)
fmt.Println(len(solutions))
}
func solveNQueens(n int) [][]string {
var res [][]string
board := make([]int, n)
backtrack(0, n, board, &res)
return res
}
func backtrack(row, n int, board []int, res *[][]string) {
if row == n {
*res = append(*res, generateBoard(board, n))
return
}
for col := 0; col < n; col++ {
if isValid(row, col, board) {
board[row] = col
backtrack(row+1, n, board, res)
}
}
}
func isValid(row, col int, board []int) bool {
for i := 0; i < row; i++ {
if board[i] == col || abs(board[i]-col) == row-i {
return false
}
}
return true
}
func generateBoard(board []int, n int) []string {
var res []string
for i := 0; i < n; i++ {
row := make([]byte, n)
for j := 0; j < n; j++ {
if board[i] == j {
row[j] = 'Q'
} else {
row[j] = '.'
}
}
res = append(res, string(row))
}
return res
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}Solution
Step 1: Understand the code logic
The code uses backtracking to find all solutions for 4 queens on a 4x4 board.Step 2: Recall known result for 4 queens
It is a classic result that there are 2 distinct solutions for 4 queens.Final Answer:
2 -> Option BQuick Check:
Number of solutions for N=4 = 2 [OK]
- Assuming only 1 solution
- Confusing number of solutions with board size
- Ignoring diagonal checks
func backtrack(row, n int, board []int, res *[][]string) {
if row == n {
*res = append(*res, generateBoard(board, n))
return
}
for col := 0; col < n; col++ {
if isValid(row, col, board) {
board[row] = col
backtrack(row+1, n, board, res)
board[row] = -1
}
}
}Solution
Step 1: Analyze backtracking state reset
After recursive call, board[row] must be reset to -1 to undo the choice.Step 2: Check if resetting is done correctly
Resetting board[row] = -1 is done inside the loop after recursion, which is correct to backtrack.Step 3: Identify the bug
If resetting is missing or incorrect, it causes wrong results because previous choices remain.Final Answer:
Missing resetting board[row] after recursion causes wrong results -> Option DQuick Check:
Backtracking needs state reset [OK]
- Not resetting board state after recursion
- Wrong base case condition
- Incorrect loop start index
Solution
Step 1: Understand performance bottlenecks
Checking conflicts using arrays for columns and diagonals is slower for large N.Step 2: Use bitmasks for fast conflict checks
Bitmasks allow O(1) checks and updates for columns and diagonals, improving speed and memory.Step 3: Compare options
Use bitmasks to track columns and diagonals instead of arrays uses bitmasks, which is a known optimization. Others increase complexity or ignore constraints.Final Answer:
Use bitmasks to track columns and diagonals instead of arrays -> Option CQuick Check:
Bitmask optimization = faster large N [OK]
- Using 2D board increases memory and slows down
- Ignoring diagonal checks causes wrong solutions
- Trying nested loops without pruning is inefficient
Ready to prove it?
Pass the test and this pattern moves unknown
