Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 85 additions & 103 deletions project_euler/problem_145/sol1.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,140 +14,122 @@
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


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)}")