Skip to content

Commit caffc9f

Browse files
committed
perf: optimize problem 145 to O(1) time complexity
1 parent 791deb4 commit caffc9f

1 file changed

Lines changed: 87 additions & 105 deletions

File tree

project_euler/problem_145/sol1.py

Lines changed: 87 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -14,140 +14,122 @@
1414
How many reversible numbers are there below one-billion (10^9)?
1515
"""
1616

17-
EVEN_DIGITS = [0, 2, 4, 6, 8]
18-
ODD_DIGITS = [1, 3, 5, 7, 9]
1917

20-
21-
def slow_reversible_numbers(
22-
remaining_length: int, remainder: int, digits: list[int], length: int
23-
) -> int:
24-
"""
25-
Count the number of reversible numbers of given length.
26-
Iterate over possible digits considering parity of current sum remainder.
27-
>>> slow_reversible_numbers(1, 0, [0], 1)
28-
0
29-
>>> slow_reversible_numbers(2, 0, [0] * 2, 2)
30-
20
31-
>>> slow_reversible_numbers(3, 0, [0] * 3, 3)
32-
100
18+
def solution(max_power: int = 9) -> int:
3319
"""
34-
if remaining_length == 0:
35-
if digits[0] == 0 or digits[-1] == 0:
36-
return 0
20+
This solution counts reversible numbers below 10^max_power
21+
using mathematical patterns instead of brute force.
3722
38-
for i in range(length // 2 - 1, -1, -1):
39-
remainder += digits[i] + digits[length - i - 1]
23+
A reversible number is a number where:
24+
n + reverse(n)
4025
41-
if remainder % 2 == 0:
42-
return 0
26+
contains only odd digits.
4327
44-
remainder //= 10
28+
Example:
29+
36 + 63 = 99
30+
409 + 904 = 1313
4531
46-
return 1
32+
Instead of checking every number one by one, we observe
33+
some repeating patterns based on the number of digits.
4734
48-
if remaining_length == 1:
49-
if remainder % 2 == 0:
50-
return 0
35+
--------------------------------------------------------
36+
Main Observations
37+
--------------------------------------------------------
5138
52-
result = 0
53-
for digit in range(10):
54-
digits[length // 2] = digit
55-
result += slow_reversible_numbers(
56-
0, (remainder + 2 * digit) // 10, digits, length
57-
)
58-
return result
39+
1. Numbers with length = 1 (mod 4)
40+
----------------------------------
41+
These lengths never work because the carry pattern becomes
42+
inconsistent while adding the number and its reverse.
5943
60-
result = 0
61-
for digit1 in range(10):
62-
digits[(length + remaining_length) // 2 - 1] = digit1
63-
64-
if (remainder + digit1) % 2 == 0:
65-
other_parity_digits = ODD_DIGITS
66-
else:
67-
other_parity_digits = EVEN_DIGITS
68-
69-
for digit2 in other_parity_digits:
70-
digits[(length - remaining_length) // 2] = digit2
71-
result += slow_reversible_numbers(
72-
remaining_length - 2,
73-
(remainder + digit1 + digit2) // 10,
74-
digits,
75-
length,
76-
)
77-
return result
44+
Examples:
45+
1 digit, 5 digits, 9 digits ...
7846
47+
Count = 0
7948
80-
def slow_solution(max_power: int = 9) -> int:
81-
"""
82-
To evaluate the solution, use solution()
83-
>>> slow_solution(3)
84-
120
85-
>>> slow_solution(6)
86-
18720
87-
>>> slow_solution(7)
88-
68720
89-
"""
90-
result = 0
91-
for length in range(1, max_power + 1):
92-
result += slow_reversible_numbers(length, 0, [0] * length, length)
93-
return result
9449
50+
2. Even length numbers
51+
-----------------------
52+
For numbers with even digits (2, 4, 6, 8 ...):
9553
96-
def reversible_numbers(
97-
remaining_length: int, remainder: int, digits: list[int], length: int
98-
) -> int:
99-
"""
100-
Count the number of reversible numbers of given length.
101-
Iterate over possible digits considering parity of current sum remainder.
102-
>>> reversible_numbers(1, 0, [0], 1)
103-
0
104-
>>> reversible_numbers(2, 0, [0] * 2, 2)
105-
20
106-
>>> reversible_numbers(3, 0, [0] * 3, 3)
107-
100
108-
"""
109-
# There exist no reversible 1, 5, 9, 13 (ie. 4k+1) digit numbers
110-
if (length - 1) % 4 == 0:
111-
return 0
54+
- Each pair of digits must produce an odd sum.
55+
- One digit in the pair must be even and the other odd.
56+
- The carry pattern stays consistent.
11257
113-
return slow_reversible_numbers(remaining_length, remainder, digits, length)
58+
Counting possibilities:
59+
- First pair has 20 valid combinations
60+
(leading digit cannot be zero)
11461
62+
- Every inner pair has 30 valid combinations
11563
116-
def solution(max_power: int = 9) -> int:
117-
"""
118-
To evaluate the solution, use solution()
119-
>>> solution(3)
120-
120
121-
>>> solution(6)
122-
18720
123-
>>> solution(7)
124-
68720
125-
"""
64+
Formula:
65+
20 * 30^(k-1)
66+
67+
where:
68+
length = 2k
69+
70+
Examples:
71+
2 digits -> 20
72+
4 digits -> 600
73+
6 digits -> 18000
74+
8 digits -> 540000
75+
76+
77+
3. Length = 3 (mod 4)
78+
----------------------
79+
These are lengths like:
80+
3, 7, 11 ...
81+
82+
Here the middle digit creates a special carry cycle,
83+
which only works for lengths of the form:
84+
85+
4j + 3
86+
87+
Formula:
88+
100 * 500^j
89+
90+
Examples:
91+
3 digits -> 100
92+
7 digits -> 50000
93+
94+
95+
--------------------------------------------------------
96+
Complexity
97+
--------------------------------------------------------
98+
99+
Time Complexity:
100+
O(max_power)
101+
102+
Space Complexity:
103+
O(1)
104+
105+
The algorithm is extremely fast because it only loops
106+
through digit lengths instead of checking every number.
107+
"""
126108
result = 0
127109
for length in range(1, max_power + 1):
128-
result += reversible_numbers(length, 0, [0] * length, length)
110+
if length % 2 == 0:
111+
# Even length 2k -> 20 x 30^(k-1)
112+
k = length // 2
113+
result += 20 * (30 ** (k - 1))
114+
elif length % 4 == 3:
115+
# Odd length 4j+3 -> 100 x 500^j
116+
j = (length - 3) // 4
117+
result += 100 * (500 ** j)
118+
# Lengths == 1 (mod 4) contribute 0 and are intentionally skipped.
119+
129120
return result
130121

131122

132123
def benchmark() -> None:
133124
"""
134125
Benchmarks
135126
"""
136-
# Running performance benchmarks...
137-
# slow_solution : 292.9300301000003
138-
# solution : 54.90970860000016
139-
140127
from timeit import timeit
141128

142129
print("Running performance benchmarks...")
143-
144-
print(f"slow_solution : {timeit('slow_solution()', globals=globals(), number=10)}")
145-
print(f"solution : {timeit('solution()', globals=globals(), number=10)}")
130+
print(f"solution : {timeit('solution()', globals=globals(), number=10_000)}")
146131

147132

148133
if __name__ == "__main__":
149134
print(f"Solution : {solution()}")
150-
benchmark()
151-
152-
# for i in range(1, 15):
153-
# print(f"{i}. {reversible_numbers(i, 0, [0]*i, i)}")
135+
benchmark()

0 commit comments

Comments
 (0)