Skip to main content
Glama

Constrained Optimization MCP Server

cryptarithmetic.py11.7 kB
""" Cryptarithmetic Puzzle Solver This module demonstrates how to solve cryptarithmetic puzzles using constraint programming. Cryptarithmetic puzzles are mathematical word problems where letters represent digits, and the goal is to find digit assignments that satisfy arithmetic equations. The classic example is: SEND + MORE = MONEY Where each letter represents a unique digit (0-9), and leading letters cannot be zero. This implementation uses Z3 SMT solver to handle the logical constraints and arithmetic relationships between the letter-digit mappings. """ import logging # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def solve_send_more_money() -> dict[str, int] | None: """ Solve the classic SEND + MORE = MONEY cryptarithmetic puzzle. Constraints: - Each letter represents a unique digit (0-9) - Leading letters (S, M) cannot be zero - SEND + MORE = MONEY must hold arithmetically Returns: Dictionary mapping letters to digits if solution exists, None otherwise """ try: from returns.result import Success from usolver_mcp.models.z3_models import ( Z3Constraint, Z3Problem, Z3Variable, Z3VariableType, ) from usolver_mcp.solvers.z3_solver import solve_problem # Define variables for each unique letter letters = ["S", "E", "N", "D", "M", "O", "R", "Y"] variables = [ Z3Variable(name=letter, type=Z3VariableType.INTEGER) for letter in letters ] constraints = [] # Each letter is a digit (0-9) for letter in letters: constraints.append( Z3Constraint(expression=f"And({letter} >= 0, {letter} <= 9)") ) # All letters represent different digits different_constraints = [] for i, letter1 in enumerate(letters): for letter2 in letters[i + 1 :]: different_constraints.append(f"{letter1} != {letter2}") if different_constraints: constraints.append( Z3Constraint(expression=f"And({', '.join(different_constraints)})") ) # Leading letters cannot be zero constraints.append(Z3Constraint(expression="And(S != 0, M != 0)")) # Main arithmetic constraint: SEND + MORE = MONEY # Convert words to numbers: SEND = 1000*S + 100*E + 10*N + D send_expr = "1000*S + 100*E + 10*N + D" more_expr = "1000*M + 100*O + 10*R + E" money_expr = "10000*M + 1000*O + 100*N + 10*E + Y" constraints.append( Z3Constraint(expression=f"({send_expr}) + ({more_expr}) == ({money_expr})") ) logger.info("Solving SEND + MORE = MONEY puzzle...") # Create problem problem = Z3Problem( variables=variables, constraints=constraints, description="SEND + MORE = MONEY cryptarithmetic puzzle", ) # Solve the constraint system result = solve_problem(problem) # Parse the result if isinstance(result, Success): solution_obj = result.unwrap() if solution_obj.is_satisfiable and solution_obj.values: return solution_obj.values logger.warning("No solution found for the puzzle") return None except Exception as e: logger.error(f"Error solving cryptarithmetic puzzle: {e}") return None def solve_general_cryptarithmetic( words: list[str], result_word: str ) -> dict[str, int] | None: """ Solve a general cryptarithmetic puzzle of the form: word1 + word2 + ... = result_word Args: words: List of addend words result_word: Sum result word Returns: Dictionary mapping letters to digits if solution exists, None otherwise """ try: from returns.result import Success from usolver_mcp.models.z3_models import ( Z3Constraint, Z3Problem, Z3Variable, Z3VariableType, ) from usolver_mcp.solvers.z3_solver import solve_problem # Find all unique letters all_letters: set[str] = set() for word in [*words, result_word]: all_letters.update(word.upper()) letters = sorted(list(all_letters)) variables = [ Z3Variable(name=letter, type=Z3VariableType.INTEGER) for letter in letters ] constraints = [] # Each letter is a digit (0-9) for letter in letters: constraints.append( Z3Constraint(expression=f"And({letter} >= 0, {letter} <= 9)") ) # All letters represent different digits different_constraints = [] for i, letter1 in enumerate(letters): for letter2 in letters[i + 1 :]: different_constraints.append(f"{letter1} != {letter2}") if different_constraints: constraints.append( Z3Constraint(expression=f"And({', '.join(different_constraints)})") ) # Leading letters cannot be zero leading_letters: set[str] = set() for word in [*words, result_word]: if word: leading_letters.add(word[0].upper()) leading_non_zero = [] for letter in leading_letters: leading_non_zero.append(f"{letter} != 0") if leading_non_zero: constraints.append( Z3Constraint(expression=f"And({', '.join(leading_non_zero)})") ) # Build arithmetic constraint def word_to_expression(word: str) -> str: word = word.upper() expr_parts = [] for i, letter in enumerate(word): power = len(word) - i - 1 if power == 0: expr_parts.append(letter) else: expr_parts.append(f"{10**power}*{letter}") return " + ".join(expr_parts) # Sum of all words equals result left_side = " + ".join(f"({word_to_expression(word)})" for word in words) right_side = word_to_expression(result_word) constraints.append(Z3Constraint(expression=f"({left_side}) == ({right_side})")) logger.info(f"Solving {' + '.join(words)} = {result_word} puzzle...") # Create problem problem = Z3Problem( variables=variables, constraints=constraints, description=f"Cryptarithmetic: {' + '.join(words)} = {result_word}", ) # Solve the constraint system result = solve_problem(problem) # Parse the result if isinstance(result, Success): solution_obj = result.unwrap() if solution_obj.is_satisfiable and solution_obj.values: return solution_obj.values return None except Exception as e: logger.error(f"Error solving general cryptarithmetic puzzle: {e}") return None def validate_solution( words: list[str], result_word: str, solution: dict[str, int] ) -> bool: """ Validate that a solution satisfies the cryptarithmetic constraints. Args: words: List of addend words result_word: Sum result word solution: Dictionary mapping letters to digits Returns: True if solution is valid, False otherwise """ try: def word_to_number(word: str) -> int: number = 0 for letter in word.upper(): number = number * 10 + solution[letter] return number # Check that all digits are unique if len(set(solution.values())) != len(solution): return False # Check that all digits are in range 0-9 if not all(0 <= digit <= 9 for digit in solution.values()): return False # Check that leading letters are not zero for word in [*words, result_word]: if word and solution[word[0].upper()] == 0: return False # Check arithmetic word_values = [word_to_number(word) for word in words] result_value = word_to_number(result_word) return sum(word_values) == result_value except (KeyError, ValueError): return False def print_solution_analysis( words: list[str], result_word: str, solution: dict[str, int] ) -> None: """Print detailed analysis of the cryptarithmetic solution.""" print("\n" + "=" * 60) print("CRYPTARITHMETIC PUZZLE SOLUTION") print("=" * 60) print("\nLetter-to-Digit Mapping:") for letter in sorted(solution.keys()): print(f" {letter} = {solution[letter]}") print(f"\nPuzzle: {' + '.join(words)} = {result_word}") print("\nArithmetic Verification:") def word_to_number(word: str) -> int: number = 0 for letter in word.upper(): number = number * 10 + solution[letter] return number word_values = [] for word in words: value = word_to_number(word) word_values.append(value) print(f" {word.upper()} = {value}") result_value = word_to_number(result_word) print(f" {result_word.upper()} = {result_value}") total = sum(word_values) print(f"\nVerification: {' + '.join(map(str, word_values))} = {total}") print(f"Expected: {result_value}") print(f"Match: {'✓' if total == result_value else '✗'}") if validate_solution(words, result_word, solution): print("\n✓ Solution is VALID!") else: print("\n✗ Solution is INVALID!") def main() -> None: """Main function to demonstrate cryptarithmetic puzzle solving.""" print("USolver Cryptarithmetic Puzzle Solver") print("=====================================") # Solve the classic SEND + MORE = MONEY puzzle print("\n1. Classic SEND + MORE = MONEY Puzzle") solution = solve_send_more_money() if solution: print_solution_analysis(["SEND", "MORE"], "MONEY", solution) else: print("No solution found for SEND + MORE = MONEY") # Solve another puzzle: TWO + TWO = FOUR print("\n" + "=" * 60) print("\n2. TWO + TWO = FOUR Puzzle") solution2 = solve_general_cryptarithmetic(["TWO", "TWO"], "FOUR") if solution2: print_solution_analysis(["TWO", "TWO"], "FOUR", solution2) else: print("No solution found for TWO + TWO = FOUR") # Solve: ONE + ONE = TWO print("\n" + "=" * 60) print("\n3. ONE + ONE = TWO Puzzle") solution3 = solve_general_cryptarithmetic(["ONE", "ONE"], "TWO") if solution3: print_solution_analysis(["ONE", "ONE"], "TWO", solution3) else: print("No solution found for ONE + ONE = TWO") def test_cryptarithmetic_solver() -> None: """Test function for pytest compatibility.""" # Test SEND + MORE = MONEY solution1 = solve_send_more_money() assert solution1 is not None, "Should find solution for SEND + MORE = MONEY" assert validate_solution( ["SEND", "MORE"], "MONEY", solution1 ), "Solution should be valid" # Test TWO + TWO = FOUR solution2 = solve_general_cryptarithmetic(["TWO", "TWO"], "FOUR") if solution2: # This puzzle may not have a solution assert validate_solution( ["TWO", "TWO"], "FOUR", solution2 ), "Solution should be valid" # Test ONE + ONE = TWO solution3 = solve_general_cryptarithmetic(["ONE", "ONE"], "TWO") if solution3: assert validate_solution( ["ONE", "ONE"], "TWO", solution3 ), "Solution should be valid" print("All cryptarithmetic tests passed!") if __name__ == "__main__": main()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Sharmarajnish/MCP-Constrained-Optimization'

If you have feedback or need assistance with the MCP directory API, please join our Discord server