Source code for mutwo.common_generators.lehmer

"""Algorithms which are related to US mathematician D.H. Lehmer."""

import abc
import typing

from mutwo import common_utilities

__all__ = ("Backtracking", "IndexBasedBacktracking")

Solution = tuple[typing.Any, ...]
ElementList = list[typing.Any]


[docs]class Backtracking(abc.ABC): """Abstract base class to implement a backtracking algorithm By inheriting from this class, various backtracking algorithms can be implemented. In order to do so the user has to override a set of abstract methods. The abstract methods include: - :abstractmethod:`Backtracking.is_valid` - :abstractmethod:`Backtracking.solution_count` - :abstractmethod:`Backtracking.append_new_element` - :abstractmethod:`Backtracking.update_last_element` - :abstractmethod:`Backtracking.can_last_element_be_updated` Furthermore it may be helpful to override the following method (even though there is a valid working implementation): - :method:`Backtracking.element_list_to_solution` Please see the methods documentation for more details. The implementation of this backtracking algorithm makes a distinction between an element list and a solution. A solution is created by an element list. A solution is the output a user wants to get, but an element list is an object which is used internally in order to solve the problem. When implementing a backtracking algorithm by using this interface the user doesn't have to make the distinction between both (and in this case treat both in the same way). The most common use case for this distinction is by having a set of items which can appear in the solution and a list of indices which item of set shall be used. In this case the element_list is actually a list of indices. This use case is implemented in the :class:`IndexBasedBacktracking` class. `Bitner and Reingold [2] credit Derrick H. Lehmer with first using the term 'backtrack' in the 1950s. <https://www.chessprogramming.org/Backtracking>`_. """ @property @abc.abstractmethod def solution_count(self) -> int: """Return expected solution size"""
[docs] @abc.abstractmethod def is_valid(self, element_list: ElementList) -> bool: """Checks if an element list provides an acceptable solution. :return: `True` if the solution is acceptable and `False` if the solution is rejected. """
[docs] @abc.abstractmethod def append_new_element(self, element_list: ElementList): """Append new element to element list. :param element_list: The element list to which a new element shall be appended. """
[docs] @abc.abstractmethod def update_last_element(self, element_list: ElementList): """Increments value of the last element in an element_list. :param element_list: The element list which last value shall be updated. This function should raise an Exception in case the last element can't be updated. """
[docs] @abc.abstractmethod def can_last_element_be_updated(self, element_list: ElementList) -> bool: """Checks if the last element of the list can be incremented. :param element_list: The element list which last value shall be checked. """
[docs] def element_list_to_solution(self, element_list: ElementList) -> Solution: """Converts an element list to the final solution :param element_list: The element list to be converted. """ return tuple(element_list)
[docs] def solve( self, return_element_list: bool = False ) -> typing.Union[Solution, tuple[Solution, ElementList]]: """Apply backtracking algorithm. :param return_element_list: If set to `True` the function will not only return the solution, but also the element list. """ element_list = [] while True: if self.is_valid(element_list): if len(element_list) < self.solution_count: self.append_new_element(element_list) else: break else: while not self.can_last_element_be_updated(element_list): element_list = element_list[:-1] if len(element_list) == 0: raise common_utilities.NoSolutionFoundError() self.update_last_element(element_list) solution = self.element_list_to_solution(element_list) if return_element_list: return solution, element_list return solution
[docs]class IndexBasedBacktracking(Backtracking): """Abstract base class for index based backtracking algorithms This class implements concrete solutions for the following methods which are inherited from the parent class :class:`Backtracking`: - :abstractmethod:`Backtracking.append_new_element` - :abstractmethod:`Backtracking.update_last_element` - :abstractmethod:`Backtracking.can_last_element_be_updated` The following methods still have to be implemented: - :abstractmethod:`Backtracking.is_valid` - :abstractmethod:`Backtracking.solution_count` (Please consult for more information the documentation of :class:`Backtracking`). Furthermore the class adds new abstract methods to be implemented by child classes: - :abstractmethod:`IndexBasedBacktracking.element_index_to_item_sequence` **Example:** >>> import itertools >>> from mutwo import common_generators >>> class QueenProblem8(common_generators.IndexBasedBacktracking): point_list = list(itertools.combinations_with_replacement(range(queen_count), 2)) point_list.extend( [tuple(reversed(point)) for point in point_list if len(set(point)) == 2] ) def element_index_to_item_sequence(self, element_index, element_list): return self.point_list @property def solution_count(self): # 8 queens problem! return 8 def is_valid(self, element_list): solution = self.element_list_to_solution(element_list) for queen0, queen1 in itertools.combinations(solution, 2): # x != x, y != y is_valid = all(value0 != value1 for value0, value1 in zip(queen0, queen1)) difference_x, difference_y = (value0 - value1 for value0, value1 in zip(queen0, queen1)) is_valid = is_valid and (difference_x != difference_y) if not is_valid: return False return True >>> queen_problem_8 = QueenProblem8() >>> queen_problem_8.solve() """
[docs] @abc.abstractmethod def element_index_to_item_sequence( self, element_index: int, element_list: ElementList ) -> typing.Sequence[typing.Any]: """Get a sequence of items to choose from for a specific element :param element_index: The index of the element for which a sequence of solutions shall be returned. :param element_list: The current element list """
[docs] def append_new_element(self, element_list: ElementList): element_list.append(0)
[docs] def update_last_element(self, element_list: ElementList): element_list[-1] += 1
[docs] def can_last_element_be_updated(self, element_list: ElementList) -> bool: max_index = len( self.element_index_to_item_sequence(len(element_list) - 1, element_list) ) return element_list[-1] + 1 < max_index
[docs] def element_list_to_solution(self, element_list: ElementList) -> Solution: return tuple( self.element_index_to_item_sequence(element_index, element_list)[element] for element_index, element in enumerate(element_list) )