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 """ queue = Queue() for edge in self.binary_constraints.keys(): queue.put(edge) while not queue.empty(): (xi, xj) = queue.get() if self._revise(xi, xj): if len(self.domains[xi]) == 0: return False for neighboring_edge in [ (a, b) for (a, b) in self.binary_constraints.keys() if a != xj and b == xi ]: queue.put(neighboring_edge) return True def _revise(self, xi, xj) -> bool: """Internal of ac_3 Makes an arc consistent by shrinking xi's domain. Parameters ---------- xi, xj Nodes of the arc Returns ------- bool True if a change was made to the domain of xi, False otherwise """ revised = False for x in set(self.domains[xi]): if not any( [(x, y) in self.binary_constraints[(xi, xj)] for y in self.domains[xj]] ): self.domains[xi].remove(x) revised = True return revised 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)) ]