| 1 | import itertools |
| 2 | import re |
| 3 | |
| 1 | 4 | def solve(puzzle: str) -> dict[str, int] | None: |
| 2 | | pass |
| 5 | # Edge Case: Empty or None puzzle input |
| 6 | if not puzzle: |
| 7 | return None |
| 8 | |
| 9 | # Parse the puzzle into words |
| 10 | # The puzzle should be in the format "A + B == C" or "A + B + C == D", etc. |
| 11 | # Support both '=' and '==' as equality operators |
| 12 | if '==' in puzzle: |
| 13 | left_side, right_side = puzzle.replace(' ', '').split('==') |
| 14 | elif '=' in puzzle: |
| 15 | left_side, right_side = puzzle.replace(' ', '').split('=', 1) |
| 16 | else: |
| 17 | return None |
| 18 | |
| 19 | # Parse the left side to identify operators and operands |
| 20 | # We need to handle +, -, and * operations |
| 21 | import re |
| 22 | |
| 23 | # Find all operators in the left side |
| 24 | operators = re.findall(r'[\+\-\*]', left_side) |
| 25 | |
| 26 | # Split the left side on operators to get operands |
| 27 | operands = re.split(r'[\+\-\*]', left_side) |
| 28 | |
| 29 | # Edge Case: No operands or no result |
| 30 | if not operands or not right_side: |
| 31 | return None |
| 32 | |
| 33 | # Remove empty strings from operands (can happen with multiple operators) |
| 34 | operands = [op for op in operands if op] |
| 35 | |
| 36 | if not operands: |
| 37 | return None |
| 38 | |
| 39 | # Collect all unique letters |
| 40 | letters = set(re.findall(r'[A-Z]', puzzle)) |
| 41 | |
| 42 | # Edge Case: No letters in puzzle |
| 43 | if not letters: |
| 44 | return None |
| 45 | |
| 46 | # Collect first letters of each word (cannot be zero) |
| 47 | first_letters = {word[0] for word in operands + [right_side]} |
| 48 | |
| 49 | # Edge Case: More than 10 unique letters (impossible to solve) |
| 50 | if len(letters) > 10: |
| 51 | return None |
| 52 | |
| 53 | # Try all permutations of digits 0-9 for the unique letters |
| 54 | for perm in itertools.permutations(range(10), len(letters)): |
| 55 | mapping = dict(zip(letters, perm)) |
| 56 | |
| 57 | # Edge Case: Leading zeros in any word |
| 58 | if any(mapping[word[0]] == 0 for word in operands + [right_side]): |
| 59 | continue |
| 60 | |
| 61 | # Convert words to numbers using the current mapping |
| 62 | try: |
| 63 | operand_values = [int(''.join(str(mapping[c]) for c in word)) for word in operands] |
| 64 | result_value = int(''.join(str(mapping[c]) for c in right_side)) |
| 65 | |
| 66 | # Evaluate the left side based on the operators |
| 67 | if not operators: |
| 68 | # Single operand case (e.g., "A == B") |
| 69 | left_value = operand_values[0] if operand_values else 0 |
| 70 | elif len(operators) == 1: |
| 71 | # Single operation case |
| 72 | op = operators[0] |
| 73 | if op == '+': |
| 74 | left_value = operand_values[0] + operand_values[1] |
| 75 | elif op == '-': |
| 76 | left_value = operand_values[0] - operand_values[1] |
| 77 | elif op == '*': |
| 78 | left_value = operand_values[0] * operand_values[1] |
| 79 | else: |
| 80 | continue # Unsupported operator |
| 81 | else: |
| 82 | # Multiple operations - evaluate left to right |
| 83 | left_value = operand_values[0] |
| 84 | for i, op in enumerate(operators): |
| 85 | if op == '+': |
| 86 | left_value += operand_values[i + 1] |
| 87 | elif op == '-': |
| 88 | left_value -= operand_values[i + 1] |
| 89 | elif op == '*': |
| 90 | left_value *= operand_values[i + 1] |
| 91 | else: |
| 92 | continue # Unsupported operator |
| 93 | |
| 94 | # Check if the equation holds |
| 95 | if left_value == result_value: |
| 96 | return mapping |
| 97 | except KeyError: |
| 98 | # This would happen if we have non-letter characters, but we already filtered for A-Z |
| 99 | continue |
| 100 | except (IndexError, ValueError): |
| 101 | # Handle any issues with operand/operator mismatch |
| 102 | continue |
| 103 | |
| 104 | # Edge Case: No valid solution found |
| 105 | return None |
| 106 | # Handled Edge Cases: Empty or None puzzle input, No addends or no result, No letters in puzzle, More than 10 unique letters, Leading zeros in any word, No valid solution found |