Skip to content

N-Queens Problem - The Backtracking Classic

"N-Queens là bài test liệu bạn có thực sự hiểu backtracking hay không." - HPN

Problem Statement

Đặt N quân hậu trên bàn cờ N×N sao cho không có 2 quân hậu nào tấn công nhau.

Quân hậu tấn công theo:

  • Hàng ngang (row)
  • Hàng dọc (column)
  • Hai đường chéo (diagonals)
N = 4, One Solution:

  . Q . .
  . . . Q
  Q . . .
  . . Q .

Queens at: (0,1), (1,3), (2,0), (3,2)

📘 Số lượng Solutions

NSolutionsDistinct (removing symmetry)
111
421
89212
1214,2001,787
14365,59645,752

Real-World Applications

DomainUse CaseTương đồng
AI PlanningTask schedulingConstraint satisfaction
CompilerRegister allocationResource conflict avoidance
VLSI DesignComponent placementNon-overlapping constraints
NetworkingFrequency assignmentInterference avoidance
Game AIMove generationValid position checking

The Backtracking Approach

Constraint Checking

python
def is_safe(board: list, row: int, col: int, n: int) -> bool:
    """
    Check if placing queen at (row, col) is safe.
    
    We only need to check:
    1. Column (upper part only, since we place row by row)
    2. Upper-left diagonal
    3. Upper-right diagonal
    """
    # Check column (rows above)
    for i in range(row):
        if board[i] == col:
            return False
    
    # Check upper-left diagonal
    i, j = row - 1, col - 1
    while i >= 0 and j >= 0:
        if board[i] == j:
            return False
        i -= 1
        j -= 1
    
    # Check upper-right diagonal
    i, j = row - 1, col + 1
    while i >= 0 and j < n:
        if board[i] == j:
            return False
        i -= 1
        j += 1
    
    return True

Basic Implementation

python
from typing import List, Optional


def solve_n_queens(n: int) -> List[List[int]]:
    """
    Find all solutions to N-Queens problem.
    
    Returns: List of solutions, each solution is [col positions per row]
             Example: [1, 3, 0, 2] means queens at (0,1), (1,3), (2,0), (3,2)
    
    Time: O(N!) - Each row has at most N-1 choices, then N-2, ...
    Space: O(N) for the board + O(N) recursion stack
    """
    solutions = []
    board = [-1] * n  # board[row] = column of queen in that row
    
    def is_safe(row: int, col: int) -> bool:
        for prev_row in range(row):
            prev_col = board[prev_row]
            
            # Same column
            if prev_col == col:
                return False
            
            # Same diagonal (|row_diff| == |col_diff|)
            if abs(prev_row - row) == abs(prev_col - col):
                return False
        
        return True
    
    def backtrack(row: int):
        if row == n:
            solutions.append(board[:])  # Found a solution!
            return
        
        for col in range(n):
            if is_safe(row, col):
                board[row] = col        # Place queen
                backtrack(row + 1)      # Recurse
                board[row] = -1         # Remove queen (backtrack)
    
    backtrack(0)
    return solutions

Optimized Implementation (O(1) Conflict Check)

python
from typing import List, Set


def solve_n_queens_optimized(n: int) -> List[List[int]]:
    """
    Optimized N-Queens with O(1) conflict checking using sets.
    
    Key insight:
    - Column conflicts: track used columns
    - Diagonal ↘: (row - col) is constant
    - Diagonal ↙: (row + col) is constant
    
    Time: O(N!) worst case, but much faster in practice
    Space: O(N) for sets
    """
    solutions = []
    board = []
    
    # Sets for O(1) conflict checking
    cols: Set[int] = set()
    diag1: Set[int] = set()  # row - col
    diag2: Set[int] = set()  # row + col
    
    def backtrack(row: int):
        if row == n:
            solutions.append(board[:])
            return
        
        for col in range(n):
            d1 = row - col
            d2 = row + col
            
            if col in cols or d1 in diag1 or d2 in diag2:
                continue  # Not safe
            
            # Place queen
            board.append(col)
            cols.add(col)
            diag1.add(d1)
            diag2.add(d2)
            
            backtrack(row + 1)
            
            # Remove queen (backtrack)
            board.pop()
            cols.remove(col)
            diag1.remove(d1)
            diag2.remove(d2)
    
    backtrack(0)
    return solutions

Production Code

python
# HPN Engineering Standard
# Implementation: N-Queens with All Features

from typing import List, Set, Generator, Optional
from dataclasses import dataclass


@dataclass
class NQueensSolver:
    """
    Production-ready N-Queens solver.
    
    Features:
    - Find all solutions
    - Find first solution only
    - Generator for memory efficiency
    - Pretty print boards
    - Count solutions without storing
    """
    
    n: int
    
    def solve_all(self) -> List[List[int]]:
        """Find all solutions."""
        return list(self._solve_generator())
    
    def solve_first(self) -> Optional[List[int]]:
        """Find first solution only (faster)."""
        try:
            return next(self._solve_generator())
        except StopIteration:
            return None
    
    def count_solutions(self) -> int:
        """Count solutions without storing them."""
        return sum(1 for _ in self._solve_generator())
    
    def _solve_generator(self) -> Generator[List[int], None, None]:
        """Generator for memory-efficient iteration."""
        n = self.n
        board = []
        cols: Set[int] = set()
        diag1: Set[int] = set()
        diag2: Set[int] = set()
        
        def backtrack(row: int):
            if row == n:
                yield board[:]
                return
            
            for col in range(n):
                d1, d2 = row - col, row + col
                
                if col in cols or d1 in diag1 or d2 in diag2:
                    continue
                
                board.append(col)
                cols.add(col)
                diag1.add(d1)
                diag2.add(d2)
                
                yield from backtrack(row + 1)
                
                board.pop()
                cols.remove(col)
                diag1.remove(d1)
                diag2.remove(d2)
        
        yield from backtrack(0)
    
    @staticmethod
    def format_board(solution: List[int]) -> str:
        """Pretty print a solution."""
        n = len(solution)
        lines = []
        
        for row in range(n):
            col = solution[row]
            line = '. ' * col + 'Q ' + '. ' * (n - col - 1)
            lines.append(line.strip())
        
        return '\n'.join(lines)
    
    @staticmethod
    def validate_solution(solution: List[int]) -> bool:
        """Verify a solution is valid."""
        n = len(solution)
        
        # Check column conflicts
        if len(set(solution)) != n:
            return False
        
        # Check diagonal conflicts
        for i in range(n):
            for j in range(i + 1, n):
                if abs(i - j) == abs(solution[i] - solution[j]):
                    return False
        
        return True


# ============================================
# VARIANTS
# ============================================

def n_queens_with_obstacles(n: int, obstacles: Set[tuple]) -> List[List[int]]:
    """
    N-Queens với một số ô bị blocked.
    
    obstacles: Set of (row, col) tuples that cannot have queens
    """
    solutions = []
    board = []
    cols: Set[int] = set()
    diag1: Set[int] = set()
    diag2: Set[int] = set()
    
    def backtrack(row: int):
        if row == n:
            solutions.append(board[:])
            return
        
        for col in range(n):
            if (row, col) in obstacles:
                continue
            
            d1, d2 = row - col, row + col
            
            if col in cols or d1 in diag1 or d2 in diag2:
                continue
            
            board.append(col)
            cols.add(col)
            diag1.add(d1)
            diag2.add(d2)
            
            backtrack(row + 1)
            
            board.pop()
            cols.remove(col)
            diag1.remove(d1)
            diag2.remove(d2)
    
    backtrack(0)
    return solutions


def n_rooks(n: int) -> int:
    """
    Count ways to place N non-attacking rooks.
    (Simpler than queens - only row/column conflicts)
    
    Answer: N! (permutations)
    """
    from math import factorial
    return factorial(n)


# ============================================
# SUDOKU SOLVER (BONUS)
# ============================================

def solve_sudoku(board: List[List[int]]) -> bool:
    """
    Solve Sudoku using backtracking.
    
    board: 9x9 grid, 0 = empty cell
    
    Modifies board in-place, returns True if solvable.
    """
    def is_valid(row: int, col: int, num: int) -> bool:
        # Check row
        if num in board[row]:
            return False
        
        # Check column
        if num in [board[r][col] for r in range(9)]:
            return False
        
        # Check 3x3 box
        box_row, box_col = 3 * (row // 3), 3 * (col // 3)
        for r in range(box_row, box_row + 3):
            for c in range(box_col, box_col + 3):
                if board[r][c] == num:
                    return False
        
        return True
    
    def find_empty() -> Optional[tuple]:
        for r in range(9):
            for c in range(9):
                if board[r][c] == 0:
                    return (r, c)
        return None
    
    def backtrack() -> bool:
        pos = find_empty()
        if pos is None:
            return True  # Solved!
        
        row, col = pos
        
        for num in range(1, 10):
            if is_valid(row, col, num):
                board[row][col] = num
                
                if backtrack():
                    return True
                
                board[row][col] = 0  # Backtrack
        
        return False
    
    return backtrack()


# ============================================
# USAGE EXAMPLE
# ============================================

if __name__ == "__main__":
    # Example 1: N-Queens
    print("=== N-Queens Solver ===\n")
    
    for n in [4, 8]:
        solver = NQueensSolver(n)
        count = solver.count_solutions()
        print(f"N={n}: {count} solutions")
        
        if n == 4:
            solutions = solver.solve_all()
            print("\nAll solutions for N=4:")
            for i, sol in enumerate(solutions):
                print(f"\nSolution {i+1}:")
                print(solver.format_board(sol))
    
    # Example 2: First solution for larger N
    print("\n=== First Solution for N=12 ===")
    solver = NQueensSolver(12)
    first = solver.solve_first()
    if first:
        print(f"Solution: {first}")
    
    # Example 3: Sudoku
    print("\n=== Sudoku Solver ===")
    
    puzzle = [
        [5, 3, 0, 0, 7, 0, 0, 0, 0],
        [6, 0, 0, 1, 9, 5, 0, 0, 0],
        [0, 9, 8, 0, 0, 0, 0, 6, 0],
        [8, 0, 0, 0, 6, 0, 0, 0, 3],
        [4, 0, 0, 8, 0, 3, 0, 0, 1],
        [7, 0, 0, 0, 2, 0, 0, 0, 6],
        [0, 6, 0, 0, 0, 0, 2, 8, 0],
        [0, 0, 0, 4, 1, 9, 0, 0, 5],
        [0, 0, 0, 0, 8, 0, 0, 7, 9],
    ]
    
    if solve_sudoku(puzzle):
        print("Solved!")
        for row in puzzle:
            print(' '.join(map(str, row)))
    else:
        print("No solution exists.")
)

Complexity Analysis

VariantTimeSpace
BasicO(N! × N)O(N)
Optimized (sets)O(N!)O(N)
Count onlyO(N!)O(N)
SudokuO(9^M)O(M)

M = empty cells in Sudoku

Backtracking Template

python
def backtrack(state):
    if is_solution(state):
        process_solution(state)
        return
    
    for choice in get_choices(state):
        if is_valid(choice, state):
            make_choice(choice, state)      # ✅ Choose
            backtrack(state)                # 🔁 Explore
            undo_choice(choice, state)      # ↩️ Unchoose

Pruning Strategies

StrategyDescriptionImpact
Early terminationStop khi tìm thấy 1 solutionSkip remaining
Constraint propagationGiảm choices dựa trên constraintsFewer branches
Variable orderingChọn variable có ít choices nhất trướcFail fast
Value orderingThử giá trị có khả năng thành công cao trướcSuccess fast

💡 HPN's Rule

"Backtracking = DFS + Pruning. Không có pruning tốt → Exponential time. Có pruning tốt → Vẫn exponential nhưng nhanh hơn nhiều."