Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

N-Queens

N queens is the problem of placing N chess queens on an N×N chessboard so that no two queens threaten each other. Thus, a solution requires that no two queens share the same row, column, or diagonal.

But for simplicity, we will implement it for 8 queens only.

Simulated Annealing

import random, math
class Board:
    def __init__(self, size=8):
        self.size = size
        # Each index is a column, value is the row of the queen
        self.state = [random.randint(0, size - 1) for _ in range(size)]

    def conflicts(self, state=None):
        """Count how many pairs of queens attack each other."""
        if state is None:
            state = self.state
        attacks = 0
        for i in range(self.size):
            for j in range(i + 1, self.size):
                # same row or same diagonal
                if state[i] == state[j] or abs(state[i] - state[j]) == abs(i - j):
                    attacks += 1
        return attacks

    def random_neighbor(self):
        new_state = self.state[:]
        col = random.randint(0, self.size - 1)
        new_row = random.randint(0, self.size - 1)
        new_state[col] = new_row
        return new_state
class SimulatedAnnealingSolver:
    def __init__(self, board, temperature=100, cooling_rate=0.99):
        self.board = board
        self.temperature = temperature
        self.cooling_rate = cooling_rate

    def solve(self):
        current = self.board.state
        current_conflicts = self.board.conflicts(current)

        while self.temperature > 0.1 and current_conflicts > 0:
            neighbor = self.board.random_neighbor()
            neighbor_conflicts = self.board.conflicts(neighbor)

            # Difference in conflicts (negative means neighbor is better)
            delta = neighbor_conflicts - current_conflicts

            # Accept if better, or sometimes if worse
            if delta < 0 or random.random() < math.exp(-delta / self.temperature):
                current = neighbor
                current_conflicts = neighbor_conflicts

            # Cool down
            self.temperature *= self.cooling_rate

        self.board.state = current
        return current_conflicts == 0  # True if solved
def print_board(state):
    size = len(state)
    for r in range(size):
        row = ""
        for c in range(size):
            row += "Q " if state[c] == r else ". "
        print(row)
    print()
def simulated_annealing_test(board):
    for i in range(100):
        solver = SimulatedAnnealingSolver(board)
        if solver.solve():
            print_board(board.state)
            return
    else:
        print("No solution found.")
board = Board()
simulated_annealing_test(board)
. . . Q . . . . 
. . . . . Q . . 
. . . . . . . Q 
. Q . . . . . . 
. . . . . . Q . 
Q . . . . . . . 
. . Q . . . . . 
. . . . Q . . . 

Genetics Algorithm

class GeneticQueens:
    def __init__(self, size=8, population_size=100, mutation_rate=0.05, max_generations=10000):
        self.size = size
        self.population_size = population_size
        self.mutation_rate = mutation_rate
        self.max_generations = max_generations

    def fitness(self, state):
        """
        Fitness = maximum non-attacking pairs - number of attacking pairs
        Higher fitness is better.
        """
        non_attacks = (self.size * (self.size - 1)) // 2
        attacks = 0
        for i in range(self.size):
            for j in range(i + 1, self.size):
                if state[i] == state[j] or abs(state[i] - state[j]) == abs(i - j):
                    attacks += 1
        return non_attacks - attacks

    def create_individual(self):
        # Each individual is a permutation of 0..7 (one queen per column)
        return [random.randint(0, self.size - 1) for _ in range(self.size)]

    def initial_population(self):
        return [self.create_individual() for _ in range(self.population_size)]

    def selection(self, population):
        """
        Tournament selection: pick two random individuals, return the fitter one.
        """
        a, b = random.sample(population, 2)
        return a if self.fitness(a) > self.fitness(b) else b

    def crossover(self, parent1, parent2):
        """
        Single-point crossover: combine parts of two parents.
        """
        point = random.randint(1, self.size - 2)
        child = parent1[:point] + parent2[point:]
        return child

    def mutate(self, individual):
        """
        Randomly change the row of a queen in one column.
        """
        if random.random() < self.mutation_rate:
            col = random.randint(0, self.size - 1)
            row = random.randint(0, self.size - 1)
            individual[col] = row
        return individual

    def solve(self):
        population = self.initial_population()
        max_fitness = (self.size * (self.size - 1)) // 2  # Perfect score (no attacks)

        for generation in range(self.max_generations):
            # Check if we already have a solution
            for individual in population:
                if self.fitness(individual) == max_fitness:
                    return individual, generation

            # Generate next generation
            new_population = []
            for _ in range(self.population_size):
                parent1 = self.selection(population)
                parent2 = self.selection(population)
                child = self.crossover(parent1, parent2)
                child = self.mutate(child)
                new_population.append(child)

            population = new_population

        return None, self.max_generations  # No solution found
ga = GeneticQueens(size=8, population_size=4, mutation_rate=0.1)
solution, generations = ga.solve()

if solution:
    print(f"Solution found in {generations} generations:\n")
    print_board(solution)
else:
    print("No solution found.")
Solution found in 2366 generations:

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