diff --git a/project_euler/problem_145/sol1.py b/project_euler/problem_145/sol1.py index 583bb03a0a90..4230d41f31da 100644 --- a/project_euler/problem_145/sol1.py +++ b/project_euler/problem_145/sol1.py @@ -14,118 +14,109 @@ How many reversible numbers are there below one-billion (10^9)? """ -EVEN_DIGITS = [0, 2, 4, 6, 8] -ODD_DIGITS = [1, 3, 5, 7, 9] - -def slow_reversible_numbers( - remaining_length: int, remainder: int, digits: list[int], length: int -) -> int: - """ - Count the number of reversible numbers of given length. - Iterate over possible digits considering parity of current sum remainder. - >>> slow_reversible_numbers(1, 0, [0], 1) - 0 - >>> slow_reversible_numbers(2, 0, [0] * 2, 2) - 20 - >>> slow_reversible_numbers(3, 0, [0] * 3, 3) - 100 +def solution(max_power: int = 9) -> int: """ - if remaining_length == 0: - if digits[0] == 0 or digits[-1] == 0: - return 0 + This solution counts reversible numbers below 10^max_power + using mathematical patterns instead of brute force. - for i in range(length // 2 - 1, -1, -1): - remainder += digits[i] + digits[length - i - 1] + A reversible number is a number where: + n + reverse(n) - if remainder % 2 == 0: - return 0 + contains only odd digits. - remainder //= 10 + Example: + 36 + 63 = 99 + 409 + 904 = 1313 - return 1 + Instead of checking every number one by one, we observe + some repeating patterns based on the number of digits. - if remaining_length == 1: - if remainder % 2 == 0: - return 0 + -------------------------------------------------------- + Main Observations + -------------------------------------------------------- - result = 0 - for digit in range(10): - digits[length // 2] = digit - result += slow_reversible_numbers( - 0, (remainder + 2 * digit) // 10, digits, length - ) - return result + 1. Numbers with length = 1 (mod 4) + ---------------------------------- + These lengths never work because the carry pattern becomes + inconsistent while adding the number and its reverse. - result = 0 - for digit1 in range(10): - digits[(length + remaining_length) // 2 - 1] = digit1 - - if (remainder + digit1) % 2 == 0: - other_parity_digits = ODD_DIGITS - else: - other_parity_digits = EVEN_DIGITS - - for digit2 in other_parity_digits: - digits[(length - remaining_length) // 2] = digit2 - result += slow_reversible_numbers( - remaining_length - 2, - (remainder + digit1 + digit2) // 10, - digits, - length, - ) - return result + Examples: + 1 digit, 5 digits, 9 digits ... + Count = 0 -def slow_solution(max_power: int = 9) -> int: - """ - To evaluate the solution, use solution() - >>> slow_solution(3) - 120 - >>> slow_solution(6) - 18720 - >>> slow_solution(7) - 68720 - """ - result = 0 - for length in range(1, max_power + 1): - result += slow_reversible_numbers(length, 0, [0] * length, length) - return result + 2. Even length numbers + ----------------------- + For numbers with even digits (2, 4, 6, 8 ...): -def reversible_numbers( - remaining_length: int, remainder: int, digits: list[int], length: int -) -> int: - """ - Count the number of reversible numbers of given length. - Iterate over possible digits considering parity of current sum remainder. - >>> reversible_numbers(1, 0, [0], 1) - 0 - >>> reversible_numbers(2, 0, [0] * 2, 2) - 20 - >>> reversible_numbers(3, 0, [0] * 3, 3) - 100 - """ - # There exist no reversible 1, 5, 9, 13 (ie. 4k+1) digit numbers - if (length - 1) % 4 == 0: - return 0 + - Each pair of digits must produce an odd sum. + - One digit in the pair must be even and the other odd. + - The carry pattern stays consistent. - return slow_reversible_numbers(remaining_length, remainder, digits, length) + Counting possibilities: + - First pair has 20 valid combinations + (leading digit cannot be zero) + - Every inner pair has 30 valid combinations -def solution(max_power: int = 9) -> int: - """ - To evaluate the solution, use solution() - >>> solution(3) - 120 - >>> solution(6) - 18720 - >>> solution(7) - 68720 + Formula: + 20 * 30^(k-1) + + where: + length = 2k + + Examples: + 2 digits -> 20 + 4 digits -> 600 + 6 digits -> 18000 + 8 digits -> 540000 + + + 3. Length = 3 (mod 4) + ---------------------- + These are lengths like: + 3, 7, 11 ... + + Here the middle digit creates a special carry cycle, + which only works for lengths of the form: + + 4j + 3 + + Formula: + 100 * 500^j + + Examples: + 3 digits -> 100 + 7 digits -> 50000 + + + -------------------------------------------------------- + Complexity + -------------------------------------------------------- + + Time Complexity: + O(max_power) + + Space Complexity: + O(1) + + The algorithm is extremely fast because it only loops + through digit lengths instead of checking every number. """ result = 0 for length in range(1, max_power + 1): - result += reversible_numbers(length, 0, [0] * length, length) + if length % 2 == 0: + # Even length 2k -> 20 x 30^(k-1) + k = length // 2 + result += 20 * (30 ** (k - 1)) + elif length % 4 == 3: + # Odd length 4j+3 -> 100 x 500^j + j = (length - 3) // 4 + result += 100 * (500**j) + # Lengths == 1 (mod 4) contribute 0 and are intentionally skipped. + return result @@ -133,21 +124,12 @@ def benchmark() -> None: """ Benchmarks """ - # Running performance benchmarks... - # slow_solution : 292.9300301000003 - # solution : 54.90970860000016 - from timeit import timeit print("Running performance benchmarks...") - - print(f"slow_solution : {timeit('slow_solution()', globals=globals(), number=10)}") - print(f"solution : {timeit('solution()', globals=globals(), number=10)}") + print(f"solution : {timeit('solution()', globals=globals(), number=10_000)}") if __name__ == "__main__": print(f"Solution : {solution()}") benchmark() - - # for i in range(1, 15): - # print(f"{i}. {reversible_numbers(i, 0, [0]*i, i)}")