Queens Problem Genetic Algorithm - Evolutionary Approach

A complete guide to solving the N queens problem using genetic algorithms. Covers permutation encoding, the non-attacking pairs fitness function, tournament selection, PMX crossover, swap mutation, a full ~40-line Python implementation, and performance comparison with backtracking.

What is a Genetic Algorithm?

A genetic algorithm (GA) is a metaheuristic search algorithm inspired by natural evolution. It maintains a population of candidate solutions, evaluates each by a fitness function, selects the fittest for reproduction via crossover, and introduces variation via mutation. Over many generations, the population converges toward high-quality solutions.

GAs are part of the broader field of evolutionary computation and are covered in AI courses alongside classical search methods. The N queens problem is a standard GA benchmark because:

  • The solution space is a set of permutations — a well-studied GA domain.
  • The fitness function (counting non-attacking pairs) is easy to compute.
  • There are 92 known optimal solutions for n=8, making verification straightforward.

For a comparison with other AI approaches (hill climbing, simulated annealing, CSP), see the 8 Queens in AI guide. For classical exact solutions, see the backtracking algorithm guide.

Encoding: Permutation Representation

The choice of representation (chromosome encoding) is critical for GA performance. For N queens, the permutation representation is the best choice:

  • A chromosome is a permutation of [0, 1, ..., n-1].
  • chromosome[row] = col means the queen in row row is in column col.
  • Because each column value appears exactly once, column conflicts are eliminated by design.

This reduces the problem to eliminating only diagonal conflicts, which are encoded in the fitness function. A random permutation like [2, 4, 6, 0, 3, 1, 7, 5] is a valid chromosome even if it has diagonal conflicts — the GA's job is to evolve toward zero-conflict permutations.

Alternative encodings and why they are worse:

  • Binary (each bit = one cell): Chromosome length n² = 64 for n=8. Most bit strings are invalid (wrong number of queens per row). Repair operators add complexity.
  • Integer per row (0..n-1, no uniqueness): Allows column conflicts, requiring the fitness function to penalize them. Larger search space.

Fitness Function: Non-Attacking Pairs

The fitness function measures how good a chromosome is. For N queens, the natural metric is the number of non-attacking queen pairs:

fitness = n*(n-1)/2 - conflicts

Where n*(n-1)/2 is the maximum possible non-attacking pairs (when no queen attacks any other), and conflicts is the number of attacking pairs.

For n=8: maximum fitness = 8×7/2 = 28. A valid solution has fitness = 28 (zero conflicts). A random permutation typically has fitness around 22–25.

We only need to count diagonal conflicts (column conflicts are impossible with permutation encoding):

def fitness(chrom):
    n = len(chrom)
    max_pairs = n * (n - 1) // 2
    conflicts = sum(
        1 for i in range(n) for j in range(i + 1, n)
        if abs(chrom[i] - chrom[j]) == abs(i - j)
    )
    return max_pairs - conflicts

The maximum fitness (28 for n=8) signals a valid solution and terminates the GA.

Selection: Tournament Selection

Selection determines which chromosomes reproduce. Tournament selection is the most common and robust method for permutation GAs:

  1. Randomly pick k individuals from the population (tournament size k, typically 2–5).
  2. The individual with the highest fitness wins and is selected as a parent.
  3. Repeat to select a second parent.

Tournament selection has desirable properties: it maintains selection pressure without over-selecting the best individual (which would reduce diversity), it is O(k) per selection, and it is easy to implement.

Alternatives considered:

  • Roulette wheel (fitness-proportionate): Problematic when fitness values have different scales; can cause premature convergence.
  • Rank-based: More robust than roulette but slower to compute.
  • Elitism: Always carry the best individual to the next generation unchanged — commonly combined with tournament selection.

Crossover: PMX (Partially Mapped Crossover)

Standard crossover operators (one-point, two-point) do not preserve permutation validity — they can produce offspring with duplicate or missing values. PMX (Partially Mapped Crossover) is the standard crossover for permutation chromosomes.

PMX procedure:

  1. Select a random crossover segment [start, end].
  2. Copy the segment from parent P1 to child C1.
  3. For positions outside the segment, copy from P2 if the value is not already in C1, otherwise follow the mapping chain to find a valid replacement.

Example for n=8:

P1: [2, 4 | 6, 0, 3 | 1, 7, 5]   segment = indices 2-4
P2: [5, 3 | 1, 7, 4 | 0, 2, 6]

C1: [_, _ | 6, 0, 3 | _, _, _]  (copy segment from P1)
    Fill remaining positions from P2 using mapping:
    1→6, 7→0, 4→3 (mapping derived from segment swap)
C1: [5, 1 | 6, 0, 3 | 7, 2, 4]  (valid permutation!)

PMX guarantees the offspring is a valid permutation, making it essential for permutation-encoded GAs.

Mutation: Swap Mutation

Mutation introduces small random changes to maintain population diversity and escape local optima. For permutation chromosomes, swap mutation is the simplest valid operator:

  1. With probability mutation_rate (typically 0.01–0.05), select two random positions i and j.
  2. Swap chromosome[i] and chromosome[j].

Swapping preserves permutation validity automatically (two values exchange positions, so all values remain present exactly once).

Other permutation mutation operators:

  • Insertion mutation: Remove one element and reinsert it at a random position. Produces minimal disruption — good for fine-tuning near-optimal solutions.
  • Scramble mutation: Randomly shuffle a sub-segment of the chromosome. More disruptive — useful for escaping local optima.
  • Inversion mutation: Reverse a sub-segment. Effective for routing problems; less naturally suited to queens.

For the queens problem, swap mutation is usually sufficient because the search landscape is relatively smooth.

Complete Python Implementation

The following complete Python implementation runs a genetic algorithm to solve the N queens problem. It uses tournament selection, PMX crossover, and swap mutation:

import random

def fitness(chrom):
    """Count non-attacking queen pairs. Max = n*(n-1)/2."""
    n = len(chrom)
    max_pairs = n * (n - 1) // 2
    conflicts = sum(
        1 for i in range(n) for j in range(i + 1, n)
        if abs(chrom[i] - chrom[j]) == abs(i - j)
    )
    return max_pairs - conflicts

def tournament_select(pop, fitnesses, k=3):
    """Select the best individual from k random candidates."""
    candidates = random.sample(range(len(pop)), k)
    best = max(candidates, key=lambda i: fitnesses[i])
    return pop[best][:]

def pmx_crossover(p1, p2):
    """Partially Mapped Crossover for permutation chromosomes."""
    n = len(p1)
    start, end = sorted(random.sample(range(n), 2))
    child = [-1] * n
    child[start:end+1] = p1[start:end+1]
    for i in list(range(0, start)) + list(range(end+1, n)):
        val = p2[i]
        while val in child[start:end+1]:
            idx = p1.index(val)
            val = p2[idx]
        child[i] = val
    return child

def swap_mutate(chrom, rate=0.02):
    """Swap two random positions with probability `rate`."""
    chrom = chrom[:]
    if random.random() < rate:
        i, j = random.sample(range(len(chrom)), 2)
        chrom[i], chrom[j] = chrom[j], chrom[i]
    return chrom

def solve_queens_ga(n=8, pop_size=100, max_gen=1000):
    """
    Genetic algorithm for N-Queens.
    Returns the solution and generation it was found.
    """
    max_fitness = n * (n - 1) // 2
    # Initialize population with random permutations
    population = [random.sample(range(n), n) for _ in range(pop_size)]

    for gen in range(max_gen):
        fitnesses = [fitness(ind) for ind in population]

        # Check for solution
        best_idx = max(range(len(fitnesses)), key=lambda i: fitnesses[i])
        if fitnesses[best_idx] == max_fitness:
            return population[best_idx], gen + 1

        # Elitism: keep best individual
        new_pop = [population[best_idx][:]]

        # Generate rest of new population
        while len(new_pop) < pop_size:
            p1 = tournament_select(population, fitnesses)
            p2 = tournament_select(population, fitnesses)
            child = pmx_crossover(p1, p2)
            child = swap_mutate(child)
            new_pop.append(child)

        population = new_pop

    # Return best found even if not optimal
    fitnesses = [fitness(ind) for ind in population]
    best = max(range(len(fitnesses)), key=lambda i: fitnesses[i])
    return population[best], max_gen


# Run the GA
solution, generations = solve_queens_ga(n=8)
print(f"Solution found in generation {generations}: {solution}")
print(f"Fitness: {fitness(solution)} / {8 * 7 // 2}")

# Print board
for row, col in enumerate(solution):
    print(". " * col + "Q " + ". " * (len(solution) - col - 1))

Performance Analysis vs Backtracking

How does the genetic algorithm compare to backtracking for the N queens problem?

MetricBacktrackingGenetic Algorithm
Finds all solutions?Yes (92 for n=8)No (stochastic)
Guaranteed to find one?YesNo (may not converge)
n=8 time<1ms~10–100ms (100 gen)
n=100 time (one solution)ImpracticalSeconds to minutes
Implementation complexityLowMedium-High
Parallelizable?With effortNaturally (island model)

When to use a genetic algorithm for queens:

  • When studying evolutionary computation and need a concrete example.
  • When the problem has additional constraints beyond standard queens (e.g., fixed queen positions, weighted boards) that make backtracking unwieldy.
  • When you want to demonstrate GA properties: selection pressure, diversity, crossover effectiveness.

When NOT to use a genetic algorithm:

  • When you need all solutions — use backtracking.
  • When you need guaranteed correctness — GAs are stochastic and can fail.
  • When n ≤ 20 — backtracking is faster and simpler.

For single-solution large-n problems, the min-conflicts heuristic outperforms GAs in both speed and simplicity.

Preguntas Frecuentes

Why use a permutation representation for the genetic algorithm queens solver?

Permutation representation eliminates column conflicts by design — each column value appears exactly once in the chromosome. This reduces the fitness function to counting only diagonal conflicts, simplifying both evaluation and crossover. Binary or integer representations require repair operators or larger penalty terms to handle invalid configurations.

How reliable is the genetic algorithm at finding a valid 8 queens solution?

With a population of 100, tournament selection, PMX crossover, and swap mutation, the GA typically finds a valid solution within 50–200 generations (~90% of runs). About 10% of runs may need restarting. This is far less reliable than backtracking (which always finds all 92 solutions) but sufficient for educational demonstrations of evolutionary computation.

What crossover operator should I use for the N queens genetic algorithm?

PMX (Partially Mapped Crossover) is the standard recommendation for permutation-encoded GAs. It preserves large sub-sequences from both parents while maintaining permutation validity. Order Crossover (OX) is another valid option that preserves relative order. One-point and two-point crossovers should not be used as they produce invalid permutations (duplicates or missing values).