Files
KI/P2/P2.py
2025-06-23 22:41:23 +02:00

450 lines
14 KiB
Python

import random
class Field:
"""
Represents the 8 Queens problem state and provides methods for state manipulation. Encodes initial state, actions, transition model, and heuristic functions.
"""
def __init__(self, init_state=None, model=None):
"""
Initialize a new Field instance for the 8 Queens problem.
Args:
init_state (list, optional): Initial queen positions [1-8] for a number of columns.
If None, generates random positions.
model (str, optional): Solution method to use ("genetic" or "backtrack").
"""
self.state = []
self.domain_values = [1, 2, 3, 4, 5, 6, 7, 8]
if init_state is None:
self.move_all_queens()
else:
self.move_all_queens(init_state)
self.threats = self.collisions(self.state)
self.fitness = 28 - self.threats
self.model = model
def get_fitness(self):
"""
Returns the fitness value of the current state.
Returns:
int: Fitness
"""
return self.fitness
def get_state(self):
"""
Returns the current state representation.
Returns:
list: List of queen row positions for each column [1-8].
"""
return self.state
def get_domain_values(self):
"""
Returns the current domain values for constraint satisfaction.
Returns:
list: Available row positions for queen placement in next column.
"""
return self.domain_values
# Actions
def set_state(self, column, row=None):
"""
Sets the queen position for a specific column and updates fitness and threats.
Args:
column (int): Column index [0-7] to place queen.
row (int, optional): Row position [1-8]. If None, places randomly.
"""
if row is None:
if column == len(self.state):
self.state.append(random.randint(1, 8))
elif 0 < row < 9:
if column < len(self.state):
self.state[column] = row
elif column == len(self.state):
self.state.append(row)
self.threats = self.collisions()
self.fitness = 28 - self.threats
def set_domain_values(self, new_domain):
"""
Updates the domain values for constraint satisfaction.
Args:
new_domain (list): New set of valid row positions.
"""
self.domain_values = new_domain
def add_queen(self, row):
"""
Adds a new queen to the next available column.
Args:
row (int): Row position [1-8] for the new queen.
"""
if len(self.get_state()) < 8:
self.set_state(len(self.get_state()), row)
def move_queen(self, column, new_row=None):
"""
Moves a queen to a new row position.
Args:
column (int): Column index [0-7] of queen to move.
new_row (int, optional): New row position [1-8]. If None, moves randomly.
"""
if column <= len(self.get_state()):
self.set_state(column, new_row)
def move_all_queens(self, new_state=None):
"""
Moves all queens to new positions.
Args:
new_state (list, optional): Complete new state configuration.
If None, moves all queens randomly.
"""
if new_state is None:
for i in range(8):
self.move_queen(i)
else:
for i, new_row in enumerate(new_state):
self.move_queen(i, new_row)
# heuristics functions
def collisions(self, current_state=None):
"""
Calculates the number of queen threats (heuristic function).
Counts horizontal and diagonal attacks between queens.
Args:
current_state (list, optional): State to evaluate. Uses self.state if None.
Returns:
float: Number of conflicting queen pairs.
"""
# wagerechte haben die gleiche row zahl stehe
# diagonale haben einen wert der um den spalten-abstand gemindert ist => gleichseitiges rechtwinkliges Dreieck
# Beachte die Spalten/ Linien Nr ist um eins verringert [0, 1, ...,7]
if current_state is None:
current_state = self.get_state()
threats = 0
for i, row_i in enumerate(current_state):
for j, row_j in enumerate(current_state):
if j is not i:
# horizontal diagonal in both sides up and down and counting "twice"
if row_i == row_j or row_j == (row_i + abs(j - i)) or row_j == (row_i - abs(j - i)):
threats += 1
# print(f"{i+1}-{row_i} <=> {j+1}-{row_j}") # Debugging
return threats / 2
def print_field(self):
"""
Displays an ASCII representation of the chess board with queens.
"""
print("\n ┌───┬───┬───┬───┬───┬───┬───┬───┐")
for row in range(8, 0, -1): # (0:8]
row_string = ""
for line in range(8):
if line < len(self.state) and row is self.state[
line]: # is there a Queen in this line (spalte) in this row
if (row + line) % 2 == 0:
row_string += "▌Q▐│"
else:
row_string += " Q │"
elif (row + line) % 2 == 0:
row_string += "███│"
else:
row_string += ""
print(f"{row}{row_string}")
if row > 1: print(" ├───┼───┼───┼───┼───┼───┼───┼───┤")
print(" └───┴───┴───┴───┴───┴───┴───┴───┘")
print(" A B C D E F G H \n")
print(f"Threats: {self.threats}")
print(f"Fitness: {self.fitness}")
def calc(self):
"""
Executes the specified solution algorithm based on the model type.
Updates based on the selected model the current state with the solution and displays the result.
"""
if self.model == "genetic":
best_field = Genetic().calc()
self.move_all_queens(best_field.get_state())
self.print_field()
elif self.model == "backtrack":
Backtrack().calc()
class Genetic:
"""
Implements genetic algorithm for solving the 8 Queens problem.
Uses fitness-based selection, crossover, and mutation operations.
"""
def __init__(self, size=1000):
"""
Initialize genetic algorithm with random population.
Args:
size (int): Population size for the genetic algorithm.
"""
self.initial_population = []
self.p_mutation = 0.1
for i in range(size):
self.initial_population.append(Field())
def random_selection(self, population):
"""
Performs fitness-proportionate selection from population.
Higher fitness individuals have higher probability of selection.
Basierend auf der Verteilung der heuristischen Werte (Fitness) soll zufällig ein Eintrag (Field) gewählt werden, d.h. je höher der heuritische Wert (Fitness) ist, umso höher soll die Wahrscheinlichkeit sein, dass ein Field ausgewählt wird
Args:
population (list): List of Field instances to select from.
Returns:
Field: Selected individual based on fitness distribution.
"""
fitness = []
for field in population:
fitness.append(field.get_fitness())
# Weighted random selection based on fitness values
chosen = random.choices(population, weights=fitness, k=1)[0]
return chosen
def mutation(self, field):
"""
Performs single random mutation on an individual by moving one queen.
Args:
field (Field): an Individual to mutate.
"""
field.move_queen(random.randint(0, 7), random.randint(1, 8))
def reproduce(self, x, y):
"""
Creates child individual through crossover of two parent individuals.
Uses single-point crossover at random position c.
Args:
x (Field): First parent individual.
y (Field): Second parent individual.
Returns:
Field: Child individual
"""
child = []
n = len(x.get_state())
c = random.randint(1, n)
# Slice operator Syntax [a:b)
child.extend(x.get_state()[:c]) # [0:c)
child.extend(y.get_state()[c:]) # [c:end)
return Field(child)
def remove_unfit(self, population, limit):
new_population = []
for i, field in enumerate(population):
if field.get_fitness() >= limit:
new_population.append(field)
population = new_population
def genetic_algorithm(self, n):
"""
Main genetic algorithm loop for evolving population.
Args:
n (int): Maximum number of generations to run.
Returns:
Field: Best individual found after n generations.
"""
current_population = self.initial_population
new_population = []
best_field = self.initial_population[0]
for i in range(n):
self.remove_unfit(current_population, 14)
for j in range(len(current_population)):
x = self.random_selection(current_population)
y = self.random_selection(current_population)
child = self.reproduce(x, y)
if random.random() < self.p_mutation:
self.mutation(child)
new_population.append(child)
if child.get_fitness() > best_field.get_fitness():
best_field = child
if best_field.get_fitness() == 28:
break
print(f"{i} {best_field.get_state()} {best_field.get_fitness()}")
if best_field.get_fitness() == 28:
break
current_population = new_population
new_population = []
return best_field
def calc(self, n=100):
"""
Executes genetic algorithm for specified number of generations.
Args:
n (int): Number of generations to evolve (default: 100).
Returns:
Field: Best solution found by genetic algorithm.
"""
best_genetic_field = self.genetic_algorithm(n)
return best_genetic_field
class Backtrack:
"""
Implements backtracking search for finding all solutions to 8 Queens problem.
Uses constraint satisfaction with consistency checking and inference.
"""
def __init__(self):
"""
Initialize backtracking solver with empty results list.
"""
self.results = []
def consistency(self, field, new_row):
"""
Checks if adding a queen at new_row maintains consistency.
Tests if the new configuration violates any constraints.
Args:
field (Field): Current partial solution.
new_row (int): Row position for new queen.
Returns:
bool: True if placement is consistent, False otherwise.
"""
current_state = field.get_state().copy()
current_state.append(new_row)
new_field = Field(current_state)
if new_field.threats > 0:
return False
else:
return True
def inference(self, field):
"""
Performs constraint propagation to reduce domain values.
Eliminates impossible row positions for the next column based on current state.
Args:
field (Field): Current partial solution.
Returns:
bool: True if domain is not empty, False if no valid moves remain.
"""
if len(field.get_state()) >= 8:
return True
# Reset domain for current column
field.set_domain_values([1, 2, 3, 4, 5, 6, 7, 8])
inferences = []
for new_row in range(1, 9):
if not self.consistency(field, new_row):
inferences.append(new_row)
for row in inferences:
if row in field.get_domain_values():
field.get_domain_values().remove(row)
if len(field.get_domain_values()) == 0:
return False
return True
def backtracing(self, field):
"""
Recursive backtracking search to find all valid solutions.
Args:
field (Field): Current partial solution.
Returns:
list: List of complete solutions found from this state.
"""
if len(field.get_state()) == 8:
return [Field(field.get_state().copy())]
solutions = []
for row in field.get_domain_values():
# old_domain_values = field.get_domain_values().copy()
if self.consistency(field, row):
field.add_queen(row)
if self.inference(field): # nur für die nächste Spalte
result = self.backtracing(field)
if len(result) != 0:
solutions.extend(result)
field.get_state().pop()
# field.domain_values = old_domain_values
return solutions
def calc(self):
for i in range(1, 9):
result = self.backtracing(Field([i]))
self.results.extend(result)
for i, result in enumerate(self.results):
print(f"{i + 1} {result.get_state()}")
def main():
print("------- Genetic -------")
gen_field = Field(model="genetic")
gen_field.calc()
print("\n------- Backtrack -------")
back_field = Field(model="backtrack")
back_field.calc()
print("\n------- My Field -------")
myField = Field()
myField.print_field()
print(myField.get_state())
myField.move_queen(1, 4)
myField.print_field()
print(myField.get_state())
main()