|
| 1 | +''' |
| 2 | +Cryptarithmetic problems are mathematical puzzles where digits are replaced by symbols. |
| 3 | +And the aim is to find unique digits(0-9) that the letters should represent, such that |
| 4 | +they satisfy the given constraints. |
| 5 | +
|
| 6 | +The cryptarithmetic problem that is needed to be solved here is: |
| 7 | + |
| 8 | + SEND |
| 9 | + + MORE |
| 10 | +--------- |
| 11 | += MONEY |
| 12 | +--------- |
| 13 | +
|
| 14 | +Distinct variables are: S, E, N, D, M, O, R, Y |
| 15 | +Domain: {0,...,9} |
| 16 | +
|
| 17 | +''' |
| 18 | + |
| 19 | +# importing the necessary libraries |
| 20 | +from typing import Generic, TypeVar, Dict, List, Optional |
| 21 | +from abc import ABC, abstractmethod |
| 22 | + |
| 23 | +# declaring a type variable V as variable type and D as domain type |
| 24 | +V = TypeVar('V') # variable type |
| 25 | +D = TypeVar('D') # domain type |
| 26 | + |
| 27 | +# this is a Base class for all constraints |
| 28 | +class Constraint(Generic[V, D], ABC): |
| 29 | + # the variables that the constraint is between |
| 30 | + def __init__(self, variables: List[V]) -> None: |
| 31 | + self.variables = variables |
| 32 | + |
| 33 | + # this is an abstract method which must be overridden by subclasses |
| 34 | + @abstractmethod |
| 35 | + def satisfied(self, assignment: Dict[V, D]) -> bool: |
| 36 | + ... |
| 37 | + |
| 38 | +# A constraint satisfaction problem consists of variables of type V |
| 39 | +# that have ranges of values known as domains of type D and constraints |
| 40 | +# that determine whether a particular variable's domain selection is valid |
| 41 | +class CSP(Generic[V, D]): |
| 42 | + def __init__(self, variables: List[V], domains: Dict[V, List[D]]) -> None: |
| 43 | + # variables to be constrained |
| 44 | + # assigning variables parameter to self.variables |
| 45 | + self.variables: List[V] = variables |
| 46 | + # domain of each variable |
| 47 | + # assigning domains parameter to self.domains |
| 48 | + self.domains: Dict[V, List[D]] = domains |
| 49 | + # assigning an empty dictionary to self.constraints |
| 50 | + self.constraints: Dict[V, List[Constraint[V, D]]] = {} |
| 51 | + # iterating over self.variables |
| 52 | + for variable in self.variables: |
| 53 | + self.constraints[variable] = [] |
| 54 | + # if the variable is not in domains, then raise a LookupError("Every variable should have a domain assigned to it.") |
| 55 | + if variable not in self.domains: |
| 56 | + raise LookupError("Every variable should have a domain assigned to it.") |
| 57 | + # this method adds constraint to variables as per their domains |
| 58 | + def add_constraint(self, constraint: Constraint[V, D]) -> None: |
| 59 | + for variable in constraint.variables: |
| 60 | + if variable not in self.variables: |
| 61 | + raise LookupError("Variable in constraint not in CSP") |
| 62 | + else: |
| 63 | + self.constraints[variable].append(constraint) |
| 64 | + |
| 65 | + # checking if the value assignment is consistent by checking all constraints |
| 66 | + # for the given variable against it |
| 67 | + def consistent(self, variable: V, assignment: Dict[V, D]) -> bool: |
| 68 | + # iterating over self.constraints[variable] |
| 69 | + for constraint in self.constraints[variable]: |
| 70 | + # if constraint not satisfied then returning False |
| 71 | + if not constraint.satisfied(assignment): |
| 72 | + return False |
| 73 | + # otherwise returning True |
| 74 | + return True |
| 75 | + |
| 76 | + # this method is performing the backtracking search to find the result |
| 77 | + def backtracking_search(self, assignment: Dict[V, D] = {}) -> Optional[Dict[V, D]]: |
| 78 | + # assignment is complete if every variable is assigned (our base case) |
| 79 | + if len(assignment) == len(self.variables): |
| 80 | + return assignment |
| 81 | + |
| 82 | + # get all variables in the CSP but not in the assignment |
| 83 | + unassigned: List[V] = [v for v in self.variables if v not in assignment] |
| 84 | + |
| 85 | + # get the every possible domain value of the first unassigned variable |
| 86 | + first: V = unassigned[0] |
| 87 | + # iterating over self.domains[first] |
| 88 | + for value in self.domains[first]: |
| 89 | + local_assignment = assignment.copy() |
| 90 | + # assign the value |
| 91 | + local_assignment[first] = value |
| 92 | + # if we're still consistent, we recurse (continue) |
| 93 | + if self.consistent(first, local_assignment): |
| 94 | + # recursively calling the self.backtracking_search method based on the local_assignment |
| 95 | + result: Optional[Dict[V, D]] = self.backtracking_search(local_assignment) |
| 96 | + # if we didn't find the result, we will end up backtracking |
| 97 | + if result is not None: |
| 98 | + return result |
| 99 | + return None |
| 100 | + |
| 101 | +# SendMoreMoneyConstraint is a subclass of Constraint class |
| 102 | +class SendMoreMoneyConstraint(Constraint[str, int]): |
| 103 | + |
| 104 | + def __init__(self, letters: List[str]) -> None: |
| 105 | + super().__init__(letters) |
| 106 | + self.letters: List[str] = letters |
| 107 | + |
| 108 | + def satisfied(self, assignment: Dict[str, int]) -> bool: |
| 109 | + # if there are duplicate values then it's not a solution |
| 110 | + if len(set(assignment.values())) < len(assignment): |
| 111 | + return False |
| 112 | + |
| 113 | + # if all variables have been assigned, check if it adds correctly |
| 114 | + if len(assignment) == len(self.letters): |
| 115 | + s: int = assignment["S"] |
| 116 | + e: int = assignment["E"] |
| 117 | + n: int = assignment["N"] |
| 118 | + d: int = assignment["D"] |
| 119 | + m: int = assignment["M"] |
| 120 | + o: int = assignment["O"] |
| 121 | + r: int = assignment["R"] |
| 122 | + y: int = assignment["Y"] |
| 123 | + send: int = s * 1000 + e * 100 + n * 10 + d |
| 124 | + more: int = m * 1000 + o * 100 + r * 10 + e |
| 125 | + money: int = m * 10000 + o * 1000 + n * 100 + e * 10 + y |
| 126 | + return send + more == money |
| 127 | + return True # no conflict |
| 128 | + |
| 129 | +if __name__ == "__main__": |
| 130 | + letters: List[str] = ["S", "E", "N", "D", "M", "O", "R", "Y"] |
| 131 | + possible_digits: Dict[str, List[int]] = {} |
| 132 | + print("******************************************************************") |
| 133 | + print("\nHere are the results:\n") |
| 134 | + for letter in letters: |
| 135 | + possible_digits[letter] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] |
| 136 | + possible_digits["M"] = [1] # so we don't get answers starting with a 0 |
| 137 | + csp: CSP[str, int] = CSP(letters, possible_digits) |
| 138 | + csp.add_constraint(SendMoreMoneyConstraint(letters)) |
| 139 | + solution: Optional[Dict[str, int]] = csp.backtracking_search() |
| 140 | + if solution is None: |
| 141 | + print("No solution found!") |
| 142 | + else: |
| 143 | + print(solution) |
| 144 | + print("\n******************************************************************") |
| 145 | + |
| 146 | +''' |
| 147 | +Sample working: |
| 148 | +
|
| 149 | +****************************************************************** |
| 150 | +
|
| 151 | +Here are the results: |
| 152 | +
|
| 153 | +{'S': 9, 'E': 5, 'N': 6, 'D': 7, 'M': 1, 'O': 0, 'R': 8, 'Y': 2} |
| 154 | +
|
| 155 | +****************************************************************** |
| 156 | +
|
| 157 | +''' |
0 commit comments