From 4e65d200d30c691fbb04e9963bb2bcc280fa0d9a Mon Sep 17 00:00:00 2001 From: fredrikr79 Date: Tue, 23 Sep 2025 12:47:42 +0200 Subject: [PATCH] ex2: init --- assignment2/.gitignore | 2 + assignment2/csp.py | 118 +++++++++++++++++++++++++++++++ assignment2/map_coloring.py | 27 +++++++ assignment2/sudoku.py | 72 +++++++++++++++++++ assignment2/sudoku_easy.txt | 9 +++ assignment2/sudoku_hard.txt | 9 +++ assignment2/sudoku_medium.txt | 9 +++ assignment2/sudoku_very_hard.txt | 9 +++ 8 files changed, 255 insertions(+) create mode 100644 assignment2/.gitignore create mode 100644 assignment2/csp.py create mode 100644 assignment2/map_coloring.py create mode 100644 assignment2/sudoku.py create mode 100644 assignment2/sudoku_easy.txt create mode 100644 assignment2/sudoku_hard.txt create mode 100644 assignment2/sudoku_medium.txt create mode 100644 assignment2/sudoku_very_hard.txt diff --git a/assignment2/.gitignore b/assignment2/.gitignore new file mode 100644 index 0000000..f5d8d8f --- /dev/null +++ b/assignment2/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.zip diff --git a/assignment2/csp.py b/assignment2/csp.py new file mode 100644 index 0000000..1d17bc6 --- /dev/null +++ b/assignment2/csp.py @@ -0,0 +1,118 @@ +from queue import Queue +from typing import Any + + +class CSP: + def __init__( + self, + variables: list[str], + domains: dict[str, set], + edges: list[tuple[str, str]], + ): + """Constructs a CSP instance with the given variables, domains and edges. + + Parameters + ---------- + variables : list[str] + The variables for the CSP + domains : dict[str, set] + The domains of the variables + edges : list[tuple[str, str]] + Pairs of variables that must not be assigned the same value + """ + self.variables = variables + self.domains = domains + + # Binary constraints as a dictionary mapping variable pairs to a set of value pairs. + # + # To check if variable1=value1, variable2=value2 is in violation of a binary constraint: + # if ( + # (variable1, variable2) in self.binary_constraints and + # (value1, value2) not in self.binary_constraints[(variable1, variable2)] + # ) or ( + # (variable2, variable1) in self.binary_constraints and + # (value1, value2) not in self.binary_constraints[(variable2, variable1)] + # ): + # Violates a binary constraint + self.binary_constraints: dict[tuple[str, str], set] = {} + for variable1, variable2 in edges: + self.binary_constraints[(variable1, variable2)] = set() + for value1 in self.domains[variable1]: + for value2 in self.domains[variable2]: + if value1 != value2: + self.binary_constraints[(variable1, variable2)].add( + (value1, value2) + ) + self.binary_constraints[(variable1, variable2)].add( + (value2, value1) + ) + + def ac_3(self) -> bool: + """Performs AC-3 on the CSP. + Meant to be run prior to calling backtracking_search() to reduce the search for some problems. + + Returns + ------- + bool + False if a domain becomes empty, otherwise True + """ + # YOUR CODE HERE (and remove the assertion below) + assert False, "Not implemented" + + def _consistent(self, var, value, assignment) -> bool: + for v in assignment: + if (var, v) in self.binary_constraints.keys(): + if (value, assignment[v]) not in self.binary_constraints[(var, v)]: + return False + if (v, var) in self.binary_constraints.keys(): + if (value, assignment[v]) not in self.binary_constraints[(v, var)]: + return False + return True + + def _backtrack(self, assignment: dict[str, Any]) -> dict | None: + if len(assignment) == len(self.variables): + return assignment # base-case + var = self._select_unassigned_variable(assignment) + for value in self._order_domain_values(var, assignment): + if not self._consistent(var, value, assignment): + continue + assignment[var] = value + if result := self._backtrack(assignment): + return result + assignment.pop(var) + return None # failure + + def backtracking_search(self) -> None | dict[str, Any]: + """Performs backtracking search on the CSP. + + Returns + ------- + None | dict[str, Any] + A solution if any exists, otherwise None + """ + + def backtrack(assignment: dict[str, Any]): + # YOUR CODE HERE (and remove the assertion below) + assert False, "Not implemented" + + return backtrack({}) + + +def alldiff(variables: list[str]) -> list[tuple[str, str]]: + """Returns a list of edges interconnecting all of the input variables + + Parameters + ---------- + variables : list[str] + The variables that all must be different + + Returns + ------- + list[tuple[str, str]] + List of edges in the form (a, b) + """ + return [ + (variables[i], variables[j]) + for i in range(len(variables) - 1) + for j in range(i + 1, len(variables)) + ] diff --git a/assignment2/map_coloring.py b/assignment2/map_coloring.py new file mode 100644 index 0000000..6a97ccf --- /dev/null +++ b/assignment2/map_coloring.py @@ -0,0 +1,27 @@ +# The map coloring problem from the text book. +# The CSP.backtrack() method needs to be implemented + +from csp import CSP, alldiff + +variables = ['WA', 'NT', 'Q', 'NSW', 'V', 'SA', 'T'] +csp = CSP( + variables=variables, + domains={variable: {'red', 'green', 'blue'} + for variable in variables}, + edges=[ + ('SA', 'WA'), + ('SA', 'NT'), + ('SA', 'Q'), + ('SA', 'NSW'), + ('SA', 'V'), + ('WA', 'NT'), + ('NT', 'Q'), + ('Q', 'NSW'), + ('NSW', 'V'), + ], +) + +print(csp.backtracking_search()) + +# Example output after implementing csp.backtracking_search(): +# {'WA': 'red', 'NT': 'green', 'Q': 'red', 'NSW': 'green', 'V': 'red', 'SA': 'blue', 'T': 'red'} diff --git a/assignment2/sudoku.py b/assignment2/sudoku.py new file mode 100644 index 0000000..091d26a --- /dev/null +++ b/assignment2/sudoku.py @@ -0,0 +1,72 @@ +# Sudoku problems. +# The CSP.ac_3() and CSP.backtrack() methods need to be implemented + +from csp import CSP, alldiff + + +def print_solution(solution): + """ + Convert the representation of a Sudoku solution, as returned from + the method CSP.backtracking_search(), into a Sudoku board. + """ + for row in range(width): + for col in range(width): + print(solution[f'X{row+1}{col+1}'], end=" ") + if col == 2 or col == 5: + print('|', end=" ") + print("") + if row == 2 or row == 5: + print('------+-------+------') + + +# Choose Sudoku problem +grid = open('sudoku_easy.txt').read().split() + +width = 9 +box_width = 3 + +domains = {} +for row in range(width): + for col in range(width): + if grid[row][col] == '0': + domains[f'X{row+1}{col+1}'] = set(range(1, 10)) + else: + domains[f'X{row+1}{col+1}'] = {int(grid[row][col])} + +edges = [] +for row in range(width): + edges += alldiff([f'X{row+1}{col+1}' for col in range(width)]) +for col in range(width): + edges += alldiff([f'X{row+1}{col+1}' for row in range(width)]) +for box_row in range(box_width): + for box_col in range(box_width): + cells = [] + edges += alldiff( + [ + f'X{row+1}{col+1}' for row in range(box_row * box_width, (box_row + 1) * box_width) + for col in range(box_col * box_width, (box_col + 1) * box_width) + ] + ) + +csp = CSP( + variables=[f'X{row+1}{col+1}' for row in range(width) for col in range(width)], + domains=domains, + edges=edges, +) + +print(csp.ac_3()) +print_solution(csp.backtracking_search()) + +# Expected output after implementing csp.ac_3() and csp.backtracking_search(): +# True +# 7 8 4 | 9 3 2 | 1 5 6 +# 6 1 9 | 4 8 5 | 3 2 7 +# 2 3 5 | 1 7 6 | 4 8 9 +# ------+-------+------ +# 5 7 8 | 2 6 1 | 9 3 4 +# 3 4 1 | 8 9 7 | 5 6 2 +# 9 2 6 | 5 4 3 | 8 7 1 +# ------+-------+------ +# 4 5 3 | 7 2 9 | 6 1 8 +# 8 6 2 | 3 1 4 | 7 9 5 +# 1 9 7 | 6 5 8 | 2 4 3 diff --git a/assignment2/sudoku_easy.txt b/assignment2/sudoku_easy.txt new file mode 100644 index 0000000..773fb6b --- /dev/null +++ b/assignment2/sudoku_easy.txt @@ -0,0 +1,9 @@ +004030050 +609400000 +005100489 +000060930 +300807002 +026040000 +453009600 +000004705 +090050200 diff --git a/assignment2/sudoku_hard.txt b/assignment2/sudoku_hard.txt new file mode 100644 index 0000000..802f7e0 --- /dev/null +++ b/assignment2/sudoku_hard.txt @@ -0,0 +1,9 @@ +102040007 +000080000 +009500304 +000607900 +540000026 +006405000 +708003400 +000010000 +200060509 diff --git a/assignment2/sudoku_medium.txt b/assignment2/sudoku_medium.txt new file mode 100644 index 0000000..2a704b3 --- /dev/null +++ b/assignment2/sudoku_medium.txt @@ -0,0 +1,9 @@ +000030040 +109700000 +000851070 +002607830 +906010207 +031502900 +010369000 +000005703 +090070000 diff --git a/assignment2/sudoku_very_hard.txt b/assignment2/sudoku_very_hard.txt new file mode 100644 index 0000000..ae023fa --- /dev/null +++ b/assignment2/sudoku_very_hard.txt @@ -0,0 +1,9 @@ +001007000 +600400300 +000030064 +380076000 +000000036 +270015000 +000020051 +700100200 +008009000