def __init__(self, input_file="sudokus.txt"): """Initize class by reading all of the sudoku puzzles""" self.sudokus = PuzzleManager(input_file)
class SudokuBacktrackingSolver(): """ This class implements a DFS based approach to solving sudoku puzzles. The DFS starts from the initial puzzle and tries assumptions about the value at the position with the possible values. It will then backtrack when it realizes a solution is inconsistent with the constraints. Additionally forward checking has been implemented to prevent the algorithm from assuming inconsistent values, thus speeding up the recursion. """ def __init__(self, input_file="sudokus.txt"): """Initize class by reading all of the sudoku puzzles""" self.sudokus = PuzzleManager(input_file) def backtracking(self): """Implements a backtracking DFS search. Returns: Return the unique solution or return False if there are multiple possible solutions """ solution = {} #Use the SudokuConstraintSolver to minimize domain size. #This will shrink the search space and consequentially #speed up the search constraintSolver = SudokuConstraintSolver() domains = constraintSolver.ac3(True, self.sudokus) #Determine uncertain positions positions = [] for position in domains: if (len(domains[position]) > 1): positions.append(position) #Recursive DFS domains = self.recursive_backtracking(positions, domains) #Check for unique solution for pos in domains: if len(domains[pos]) == 1: solution[pos] = domains[pos].pop() else: return False self.sudokus.save_puzzle(solution) self.sudokus.print_puzzle() return solution def recursive_backtracking(self, variables, domains): """Recursive helper for backtracking search. Args: variables: positions in puzzle with uncertain values domains: all possible value for each position Returns: Return the unique solution or return False if there are multiple possible solutions """ #Return value if all positions in puzzle are solved if len(variables) == 0: return domains #Get position with smallest domain var_pos = variables.pop( variables.index(self.min_constraing_val(domains, variables))) constraints = self.get_constraints(var_pos[0], var_pos[1]) for value in domains[var_pos]: inconsistent = False #Check if value in domain is inconsistent with constraints for (current_position, constraint_position) in constraints: if constraint_position not in variables and domains[ constraint_position][0] == value: inconsistent = True if not inconsistent: #Assume this possible value and recursively check solution that have it. tmpDomain = self.forward_checking(deepcopy(domains), var_pos, value) if not tmpDomain: return False tmpDomain[var_pos] = [value] result = self.recursive_backtracking(deepcopy(variables), deepcopy(tmpDomain)) if result: return result return False def forward_checking(self, domain, var, value): """Prevents Assignments that garuntee a failed result later in the search. i.e. if a possible value would violate a constraint of the puzzle, then we will remove that value from the domain dictionary Args: domain: dictionary with positions within puzzle are keys are hashed to lists possible values var: the postion being considered for assignment =[row,column] value: teh value being considered for assignment Returns: returns the dictionary of domains returns false if a position does not have any possible values """ constraints = self.get_constraints(var[0], var[1]) for (col_1, col_2) in constraints: if value in domain[col_2]: domain[col_2].pop(domain[col_2].index(value)) if len(domain[col_2]) < 1: return False return domain def min_constraing_val(self, domains, positions): """Finds the position in the puzzle with the smallest possible domain (fewest possible values). Args: domains: dictionary with positions within puzzle are keys are hashed to lists possible values positions: list of all positions within puzzle Returns: the position of the smallest domain """ smallest = "" smallest_size = 9 for pos in positions: if len(domains[pos]) < smallest_size: smallest = pos smallest_size = len(domains[pos]) return smallest def get_constraints(self, current_row, current_col): """Finds all the constraints that apply to a specified position. Adds all positions in the same column, row, or box and adds them as constraints for the specified position. Args: current_row: specifies the row position in the puzzle to find constraints for. current_col: specifies the column position in the puzzle to find constraints for. Returns: A list of constraints as tuples where the first value is the position index to being constrained by the second postion. """ constraints = [] #ROW constraints for row in ROWS: if row != current_row: constraints.append( (current_row + current_col, row + current_col)) #COL Constraints for col in COLS: if col != current_col: constraints.append( (current_row + current_col, current_row + col)) #BOX Constraints box_cols = ['123', '456', '789'] box_rows = ['ABC', 'DEF', 'GHI'] for b_rows in box_rows: if current_row in b_rows: box_row = b_rows for b_col in box_cols[int(math.ceil(float(current_col) / 3) - 1)]: for b_row in box_row: if current_row != b_row and current_col != b_col: constraints.append( (current_row + current_col, b_row + b_col)) return constraints
class SudokuConstraintSolver(object): """ The SudokuConstraintSolver class is a constraint based algorithm used to reduce the search space. It utilizes an AC3 algorithm described below. """ def __init__(self, input_file="sudokus.txt"): """Initize class by reading all of the sudoku puzzles""" self.sudokus = PuzzleManager(input_file) def ac3(self, return_domain=False, input_pm=False): """Utilizes the Arc Consistency Algorithm #3 to reduce the domains of possible values in the puzzle. This algorithm will initialize the domains and constraints of puzzle. Then apply its constraints to the reduce domains of constrained values. It should be noted that more often than not, AC-3 algorithm will not produce a unique solution but offers significant speed up when used in conjuction with DFS based methods. More info can be found at https://en.wikipedia.org/wiki/AC-3_algorithm Args: return_domain: specifies whether to return the resulting domain or unique solution. input_pm: class will not be using its own puzzle manager Returns: If return_domain is True, return the dictionary of possible domain values for entire puzzle. Otherwise, return the unique solution or return False if there are multiple solutions """ if input_pm: self.sudokus = input_pm #Determine all possible values at each position in the puzzle domains = self.sudokus.init_domains() solution = {} #Find all of the initial constraints for the sudoku puzzle queue = [] for row in ROWS: for col in COLS: if self.sudokus.get_value(row, col) == int(PLACEHOLDER): queue += self.get_constraints(row, col) #Loop while constraints are still being applied while queue: (pos_1, pos_2) = queue.pop() #Remove values inconsistent with the constrains from the domain of possible values if self.remove_inconsistent_values(domains, pos_1, pos_2): for (pos_1, pos_3) in self.get_constraints(pos_1[0], pos_1[1]): queue.append((pos_3, pos_1)) if return_domain: return domains #Check for unique solution for pos in domains: if len(domains[pos]) == 1: solution[pos] = domains[pos].pop() else: return False self.sudokus.save_puzzle(solution) self.sudokus.print_puzzle() return solution def get_constraints(self, current_row, current_col): """Finds all the constraints that apply to a specified position. Adds all positions in the same column, row, or box and adds them as constraints for the specified position. Args: current_row: specifies the row position in the puzzle to find constraints for. current_col: specifies the column position in the puzzle to find constraints for. Returns: A list of constraints as tuples where the first value is the position index to being constrained by the second value. """ constraints = [] #ROW constraints for row in ROWS: if row != current_row: constraints.append((current_row + current_col, row + current_col)) #COL Constraints for col in COLS: if col != current_col: constraints.append((current_row + current_col, current_row + col)) #BOX Constraints box_cols = ['123', '456', '789'] box_rows = ['ABC', 'DEF', 'GHI'] for b_rows in box_rows: if current_row in b_rows: box_row = b_rows for b_col in box_cols[int(math.ceil(float(current_col)/3)-1)]: for b_row in box_row: if current_row != b_row and current_col != b_col: constraints.append((current_row + current_col, b_row + b_col)) return constraints def remove_inconsistent_values(self, domain, pos_1, pos_2): """Shrinks the domain of a position by removing values Test each domain value in position 1 and if no domain value in position 2 is consistent with that domain value (i.e. can only be that value), remove that domain value from position 1. Args: domain: a dictionary of all possible domain values pos_1: the position of the domain that we are trying to reduce pos_2: the position of the current constraint Returns: returns True if domain of position 1 was reduced and False otherwise """ removed = False for val_1 in domain[pos_1]: found = False for val_2 in domain[pos_2]: if val_1 != val_2: found = True if not found: domain[pos_1].pop(domain[pos_1].index(val_1)) removed = True return removed
class SudokuBacktrackingSolver(): """ This class implements a DFS based approach to solving sudoku puzzles. The DFS starts from the initial puzzle and tries assumptions about the value at the position with the possible values. It will then backtrack when it realizes a solution is inconsistent with the constraints. Additionally forward checking has been implemented to prevent the algorithm from assuming inconsistent values, thus speeding up the recursion. """ def __init__(self, input_file="sudokus.txt"): """Initize class by reading all of the sudoku puzzles""" self.sudokus = PuzzleManager(input_file) def backtracking(self): """Implements a backtracking DFS search. Returns: Return the unique solution or return False if there are multiple possible solutions """ solution = {} #Use the SudokuConstraintSolver to minimize domain size. #This will shrink the search space and consequentially #speed up the search constraintSolver = SudokuConstraintSolver() domains = constraintSolver.ac3(True, self.sudokus) #Determine uncertain positions positions = [] for position in domains: if(len(domains[position]) > 1): positions.append(position) #Recursive DFS domains = self.recursive_backtracking(positions, domains) #Check for unique solution for pos in domains: if len(domains[pos]) == 1: solution[pos] = domains[pos].pop() else: return False self.sudokus.save_puzzle(solution) self.sudokus.print_puzzle() return solution def recursive_backtracking(self, variables, domains): """Recursive helper for backtracking search. Args: variables: positions in puzzle with uncertain values domains: all possible value for each position Returns: Return the unique solution or return False if there are multiple possible solutions """ #Return value if all positions in puzzle are solved if len(variables) == 0: return domains #Get position with smallest domain var_pos = variables.pop(variables.index(self.min_constraing_val(domains, variables))) constraints = self.get_constraints(var_pos[0], var_pos[1]) for value in domains[var_pos]: inconsistent = False #Check if value in domain is inconsistent with constraints for (current_position, constraint_position) in constraints: if constraint_position not in variables and domains[constraint_position][0] == value: inconsistent = True if not inconsistent: #Assume this possible value and recursively check solution that have it. tmpDomain = self.forward_checking(deepcopy(domains), var_pos, value) if not tmpDomain: return False tmpDomain[var_pos] = [value] result = self.recursive_backtracking(deepcopy(variables), deepcopy(tmpDomain)) if result: return result return False def forward_checking(self, domain, var, value): """Prevents Assignments that garuntee a failed result later in the search. i.e. if a possible value would violate a constraint of the puzzle, then we will remove that value from the domain dictionary Args: domain: dictionary with positions within puzzle are keys are hashed to lists possible values var: the postion being considered for assignment =[row,column] value: teh value being considered for assignment Returns: returns the dictionary of domains returns false if a position does not have any possible values """ constraints = self.get_constraints(var[0], var[1]) for (col_1, col_2) in constraints: if value in domain[col_2]: domain[col_2].pop(domain[col_2].index(value)) if len(domain[col_2]) < 1: return False return domain def min_constraing_val(self, domains, positions): """Finds the position in the puzzle with the smallest possible domain (fewest possible values). Args: domains: dictionary with positions within puzzle are keys are hashed to lists possible values positions: list of all positions within puzzle Returns: the position of the smallest domain """ smallest = "" smallest_size = 9 for pos in positions: if len(domains[pos]) < smallest_size: smallest = pos smallest_size = len(domains[pos]) return smallest def get_constraints(self, current_row, current_col): """Finds all the constraints that apply to a specified position. Adds all positions in the same column, row, or box and adds them as constraints for the specified position. Args: current_row: specifies the row position in the puzzle to find constraints for. current_col: specifies the column position in the puzzle to find constraints for. Returns: A list of constraints as tuples where the first value is the position index to being constrained by the second postion. """ constraints = [] #ROW constraints for row in ROWS: if row != current_row: constraints.append((current_row + current_col, row + current_col)) #COL Constraints for col in COLS: if col != current_col: constraints.append((current_row + current_col, current_row + col)) #BOX Constraints box_cols = ['123', '456', '789'] box_rows = ['ABC', 'DEF', 'GHI'] for b_rows in box_rows: if current_row in b_rows: box_row = b_rows for b_col in box_cols[int(math.ceil(float(current_col)/3)-1)]: for b_row in box_row: if current_row != b_row and current_col != b_col: constraints.append((current_row + current_col, b_row + b_col)) return constraints