Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 43

N-Queens: Backtracking with Pruning

Recommended resources

Hello! Welcome back to our module on Recursion and Backtracking.

In our last lesson, we built a solid foundation by applying the "Choose, Explore, Unchoose" backtracking template to generate combinatorial objects like subsets and permutations. We learned how to manage state using a start_index for problems where order doesn't matter, and a used array for when it does.

Today, we shift from generating all possibilities to finding solutions that satisfy a complex set of rules. We will tackle the N-Queens problem, a classic interview question that perfectly illustrates backtracking with constraint satisfaction and pruning. Your goal for this lesson is to solve the N-Queens problem by implementing a backtracking algorithm that efficiently prunes the search space of invalid solutions.

This problem is a staple in technical interviews because it requires you to move beyond a basic template and think critically about how to check constraints efficiently—a skill highly valued in any software or AI role.

1. The N-Queens Problem

The problem is stated as follows: Given an integer N, place N chess queens on an N×N chessboard so that no two queens threaten each other. This means no two queens can share the same row, column, or diagonal.

N-Queens Backtracking Search Tree and Solution
CAPTION: A visualization of the backtracking search tree for the 4-Queens problem. The algorithm explores placing queens row by row. Invalid placements (conflicts) cause branches to be "pruned" (marked with 'X'), saving significant computation. A path from the root to a full set of N queens represents a valid solution.

A brute-force approach of trying all possible placements of N queens on N² squares is computationally infeasible. A smarter approach is needed, which begins with a powerful initial observation.

N-Queens - Backtracking - Leetcode 51 - Python

To start, let's get a clear definition of the problem and its rules. The NeetCode video 'N-Queens - Backtracking' provides an excellent introduction.

Watch the first two minutes of the video (00:29 - 02:03). Pay close attention to how a queen attacks and the visual representation of the constraints.

The first and most important form of pruning comes from this constraint: since no two queens can be in the same row, we must place exactly one queen in each row. This insight reduces the problem from choosing N squares out of N² to deciding which column to place a queen in for each row. This transforms the problem into a structure perfect for our backtracking template.

Our recursive function, let's call it backtrack(row), will be responsible for placing a queen in the given row.


2. A Backtracking Strategy with Efficient Pruning

Our strategy follows the template from the previous lesson:

  1. Base Case: If we have successfully placed a queen in every row (i.e., row == N), we have found a valid solution. We record the board configuration and return.
  2. Choices: For the current row, iterate through all possible columns (col from 0 to N-1).
  3. Constraint Check (Pruning): For each column, check if placing a queen at (row, col) is valid. A placement is valid only if the square is not under attack by any previously placed queens.
  4. Explore: If the placement is valid:
    • Choose: Place the queen at (row, col) and update our state to reflect this choice.
    • Explore: Call backtrack(row + 1) to solve for the next row.
    • Unchoose: Remove the queen from (row, col) and revert our state. This is the backtracking step that allows us to explore other columns in the current row.

The most critical part of this algorithm is the constraint check. A naive check would involve iterating through all previously placed queens, which would be slow. For an O(1) check, we need a way to instantly know if a column or diagonal is already occupied. Given your familiarity with data structures, you might already be thinking of using hash sets for this.

  • Column Check: A hash set (occupied_cols) can store the indices of all columns that contain a queen. Checking if a column c is free is an O(1) lookup.
  • Diagonal Check: This is the clever part. We can observe a mathematical property of diagonals on a grid:
    • For any square (r, c) on a positive diagonal (top-left to bottom-right, like /), the value of r + c is constant.
    • For any square (r, c) on a negative diagonal (top-right to bottom-left, like \), the value of r - c is constant.

We can use two more hash sets, pos_diagonals and neg_diagonals, to store these constant values for each occupied diagonal. This allows us to check for diagonal attacks in O(1).

The following resource explains this core optimization strategy in detail.

13.4 N-Queens Problem

The article '13.4 N-Queens Problem' from Hello Algo provides clear diagrams and explanations for this efficient pruning strategy.

Read the sections '1. Row-By-Row Placement Strategy' and '2. Column and Diagonal Pruning'. Focus on Figure 13-18 and the accompanying text that explains how row - col and row + col uniquely identify diagonals.

Test your understanding!

Consider a standard 8x8 chessboard. If a queen is placed at (row=3, col=5):

  1. What value would be stored in the pos_diagonals set?
  2. What value would be stored in the neg_diagonals set?
Show answer
  1. pos_diagonals would store r + c = 3 + 5 = 8.
  2. neg_diagonals would store r - c = 3 - 5 = -2.

Any other square (r', c') where r' + c' = 8 or r' - c' = -2 would be under attack from this queen.


3. Code Implementation

Now, let's put all these pieces together into a Python implementation. The following video walks through the code, combining the row-by-row strategy with the three-set pruning technique.

N-Queens - Backtracking - Leetcode 51 - Python

This segment from the NeetCode video demonstrates a complete Python solution. It's a clean implementation that directly reflects the concepts we've just discussed.

Watch from 03:39 to 17:41. This is the core of the lesson. Observe how the three sets (col, posDiag, negDiag) are used to implement the O(1) constraint check and how the 'Choose, Explore, Unchoose' pattern is structured within the recursive calls.

Let's summarize the structure of the solution presented in the video:

class Solution:
    def solveNQueens(self, n: int) -> list[list[str]]:
        # Sets to store occupied columns and diagonals
        col_set = set()
        pos_diag_set = set() # (r + c)
        neg_diag_set = set() # (r - c)
        
        res = []
        board = [["."] * n for i in range(n)]

        def backtrack(r):
            # Base Case: All queens have been placed
            if r == n:
                # Format the board from a list of lists to a list of strings
                copy = ["".join(row) for row in board]
                res.append(copy)
                return

            # Recursive Step: Try placing a queen in each column of the current row 'r'
            for c in range(n):
                # Constraint Check (Pruning)
                if c in col_set or (r + c) in pos_diag_set or (r - c) in neg_diag_set:
                    continue
                
                # 1. Choose
                col_set.add(c)
                pos_diag_set.add(r + c)
                neg_diag_set.add(r - c)
                board[r][c] = "Q"
                
                # 2. Explore
                backtrack(r + 1)
                
                # 3. Unchoose (Backtrack)
                col_set.remove(c)
                pos_diag_set.remove(r + c)
                neg_diag_set.remove(r - c)
                board[r][c] = "."

        backtrack(0)
        return res

This implementation is a model answer for an interview setting. It's clean, efficient, and demonstrates a deep understanding of both backtracking and how to optimize constraint checks.


4. Complexity Analysis

Finally, let's briefly analyze the algorithm's performance.

  • Time Complexity: The search space is roughly bounded by N! (N choices for the first row, N-1 for the second, and so on). For each valid solution found, we take O(N^2) time to copy the board. Therefore, a loose upper bound is O(N! * N^2). While this is a worst-case analysis, the heavy pruning makes the actual runtime much better for typical values of N. Answering O(N!) in an interview is generally sufficient.

  • Space Complexity: The primary space is used to store the board, which is O(N^2). The recursion depth is N, so the call stack uses O(N) space. The three sets also use O(N) space each. Thus, the space complexity is dominated by the board, making it O(N^2).

The "Hello Algo" page you reviewed earlier has a good summary of this under its "Code Implementation" section.


Conclusion

In this lesson, you've applied the backtracking template to a more complex constraint satisfaction problem. You've gone beyond simple generation and implemented intelligent pruning to make an intractable problem solvable.

Key Takeaways:

  • N-Queens is a classic constraint satisfaction problem solved with backtracking.
  • The first level of pruning is to adopt a row-by-row placement strategy, reducing the decision at each step to "which column to choose?"
  • The key to an efficient solution is O(1) constraint checking. This is achieved by using hash sets (or boolean arrays) to track occupied columns and diagonals.
  • Diagonals can be uniquely identified by the constant values of row + c (positive diagonals) and row - c (negative diagonals).

In our next and final lesson of this module, we'll explore how to optimize recursive algorithms that solve the same subproblem multiple times. We will introduce memoization, a technique that caches the results of expensive function calls, which serves as a perfect bridge from recursion to our next major topic: Dynamic Programming.