Backtracking

Introduction to backtracking algorithms. Covers solving constraint satisfaction problems like N-Queens and Sudoku using backtracking.


N-Queens Problem

Introduction

The N-Queens problem is a classic constraint satisfaction problem that asks how to place N chess queens on an N×N chessboard so that no two queens threaten each other. This means no two queens can be in the same row, column, or diagonal.

Problem Definition

Given an integer N, the task is to find all possible arrangements of N queens on an N×N chessboard such that no two queens can attack each other. A queen can attack any piece in the same row, column, or diagonal. Therefore, a valid solution requires that each row and column contains exactly one queen and that no two queens share a diagonal.

Detailed Explanation

The N-Queens problem serves as an excellent example for demonstrating the power of backtracking algorithms. The brute-force approach of trying all possible placements of queens quickly becomes infeasible as N increases. Backtracking offers a more efficient way to explore the solution space by incrementally building candidate solutions and abandoning partial candidates as soon as they violate the constraints.

Constraints

The primary constraints in the N-Queens problem are:

  • Row Constraint: Only one queen can be placed in each row.
  • Column Constraint: Only one queen can be placed in each column.
  • Diagonal Constraint: No two queens can be placed on the same diagonal (both main and anti-diagonals).

Solving with Backtracking: A Step-by-Step Approach

Here's a detailed explanation of how to solve the N-Queens problem using backtracking:

  1. Representation: The chessboard can be represented as a 2D array or, more efficiently, as a 1D array where the index represents the row, and the value represents the column where the queen is placed in that row. For example, if `board[i] = j`, it means there is a queen at row `i` and column `j`.
  2. Recursive Function (Backtracking): We define a recursive function, typically named `solveNQueensUtil`, that takes the following parameters:
    • board: The array representing the board configuration.
    • row: The current row being considered for placing a queen.
    • N: The size of the chessboard.
    • solutions: A list (or similar data structure) to store valid solutions.
  3. Base Case: If `row` equals `N`, it means we have successfully placed N queens without any conflicts. This is a valid solution. We add a copy of the current board configuration to the `solutions` list. The recursion stops here.
  4. Recursive Step: For each column `col` in the current `row`:
    1. Check for Safety: Check if placing a queen at `board[row] = col` is safe, meaning it doesn't conflict with any previously placed queens. This involves checking the column and diagonals.
    2. If Safe:
      • Place the queen: `board[row] = col`.
      • Recursively call `solveNQueensUtil(board, row + 1, N, solutions)` to try placing a queen in the next row.
      • Backtrack: After the recursive call returns, *remove* the queen (set `board[row]` to some invalid value like -1, or 0 if that means empty on your implementation). This is the crucial backtracking step. It allows us to explore other possibilities.
    3. If Not Safe: Continue to the next column `col`.
  5. Safety Check (isSafe function): This function determines if placing a queen at `board[row] = col` is safe. It needs to check:
    • Column: Iterate through all previous rows (0 to `row - 1`) and see if any queen is in the same column (`board[i] == col`).
    • Diagonals: Iterate through all previous rows (0 to `row - 1`) and check both diagonals:
      • Main Diagonal (upper-left to lower-right): Check if `abs(row - i) == abs(col - board[i])`. This ensures that the absolute difference in row indices is equal to the absolute difference in column indices.
      • Anti-Diagonal (upper-right to lower-left): Check if `abs(row - i) == abs(col + board[i])`. Be careful of integer overflow or using signed vs. unsigned numbers. The logic here is that the sum of the row and column indices will be the same along an anti-diagonal. So check if `(i + board[i]) == (row + col)`. If `N` is large enough, then a safe check is `abs(row - i) == abs(col - board[i])`.
  6. Initial Call: The main function initializes the board (usually with all values set to -1 to indicate empty) and calls `solveNQueensUtil(board, 0, N, solutions)` to start the backtracking process from the first row.

Example Code (Python)

 def solveNQueens(n):
    """
    Solves the N-Queens problem and returns a list of all possible solutions.

    Args:
        n: The size of the chessboard (N).

    Returns:
        A list of lists, where each inner list represents a solution. Each solution
        is a list of integers, where the i-th element represents the column
        where the queen is placed in the i-th row.
    """

    def isSafe(board, row, col):
        """
        Checks if placing a queen at board[row][col] is safe (doesn't conflict).
        """
        for i in range(row):
            if board[i] == col or abs(row - i) == abs(col - board[i]):
                return False
        return True

    def solveNQueensUtil(board, row, solutions):
        """
        Recursive helper function to solve the N-Queens problem using backtracking.
        """
        if row == n:
            solutions.append(board[:])  # Append a COPY of the board
            return

        for col in range(n):
            if isSafe(board, row, col):
                board[row] = col
                solveNQueensUtil(board, row + 1, solutions)
                board[row] = -1  # Backtrack

    solutions = []
    board = [-1] * n  # Initialize the board with -1 indicating no queen

    solveNQueensUtil(board, 0, solutions)
    return solutions

# Example usage:
n = 4
solutions = solveNQueens(n)

for solution in solutions:
    print(solution)


    # Visual Representation for the first solution
    if solutions:
        print("\nVisual Representation of the First Solution:")
        for i in range(n):
            row_str = ""
            for j in range(n):
                if solution[i] == j:
                    row_str += "Q "
                else:
                    row_str += ". "
            print(row_str) 

Time and Space Complexity

* Time Complexity: O(N!), in the worst case, we might explore all possible permutations of placing queens. This is a very loose upper bound, and the actual runtime is significantly better due to backtracking pruning large portions of the search space. * Space Complexity: O(N), primarily due to the recursion depth and the storage required for the board. The list of solutions can grow much larger, but that is the space to *store* the solutions, not the space needed to *find* a solution.

Optimizations and Variations

* Bit Manipulation: Bit manipulation techniques can be used to represent the chessboard and the occupied columns and diagonals more efficiently. This can significantly improve the performance of the `isSafe` function. * Symmetry: Exploit the symmetry of the chessboard to reduce the search space. For example, if N is even, you only need to explore placements in the first half of the board and then reflect the solutions to get the other half. * Parallelization: The backtracking search can be parallelized by dividing the search space among multiple processors.

Conclusion

The N-Queens problem is a valuable exercise for understanding and applying the backtracking algorithm. It showcases how backtracking can efficiently explore a large search space by intelligently pruning branches that lead to infeasible solutions. The problem can be adapted and extended to various other constraint satisfaction scenarios.