Skip to content

Commit 562cf31

Browse files
Improve Project Euler problem 074 solution 2 (#5803)
* Fix statement * Improve solution * Fix * Add tests
1 parent 533eea5 commit 562cf31

File tree

1 file changed

+109
-88
lines changed

1 file changed

+109
-88
lines changed

Diff for: project_euler/problem_074/sol2.py

+109-88
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,143 @@
11
"""
2-
Project Euler Problem 074: https://projecteuler.net/problem=74
2+
Project Euler Problem 074: https://projecteuler.net/problem=74
33
4-
Starting from any positive integer number
5-
it is possible to attain another one summing the factorial of its digits.
4+
The number 145 is well known for the property that the sum of the factorial of its
5+
digits is equal to 145:
66
7-
Repeating this step, we can build chains of numbers.
8-
It is not difficult to prove that EVERY starting number
9-
will eventually get stuck in a loop.
7+
1! + 4! + 5! = 1 + 24 + 120 = 145
108
11-
The request is to find how many numbers less than one million
12-
produce a chain with exactly 60 non repeating items.
9+
Perhaps less well known is 169, in that it produces the longest chain of numbers that
10+
link back to 169; it turns out that there are only three such loops that exist:
1311
14-
Solution approach:
15-
This solution simply consists in a loop that generates
16-
the chains of non repeating items.
17-
The generation of the chain stops before a repeating item
18-
or if the size of the chain is greater then the desired one.
19-
After generating each chain, the length is checked and the
20-
counter increases.
21-
"""
12+
169 → 363601 → 1454 → 169
13+
871 → 45361 → 871
14+
872 → 45362 → 872
2215
23-
factorial_cache: dict[int, int] = {}
24-
factorial_sum_cache: dict[int, int] = {}
16+
It is not difficult to prove that EVERY starting number will eventually get stuck in a
17+
loop. For example,
2518
19+
69 → 363600 → 1454 → 169 → 363601 (→ 1454)
20+
78 → 45360 → 871 → 45361 (→ 871)
21+
540 → 145 (→ 145)
2622
27-
def factorial(a: int) -> int:
28-
"""Returns the factorial of the input a
29-
>>> factorial(5)
30-
120
23+
Starting with 69 produces a chain of five non-repeating terms, but the longest
24+
non-repeating chain with a starting number below one million is sixty terms.
3125
32-
>>> factorial(6)
33-
720
26+
How many chains, with a starting number below one million, contain exactly sixty
27+
non-repeating terms?
3428
35-
>>> factorial(0)
36-
1
37-
"""
38-
39-
# The factorial function is not defined for negative numbers
40-
if a < 0:
41-
raise ValueError("Invalid negative input!", a)
29+
Solution approach:
30+
This solution simply consists in a loop that generates the chains of non repeating
31+
items using the cached sizes of the previous chains.
32+
The generation of the chain stops before a repeating item or if the size of the chain
33+
is greater then the desired one.
34+
After generating each chain, the length is checked and the counter increases.
35+
"""
36+
from math import factorial
4237

43-
if a in factorial_cache:
44-
return factorial_cache[a]
38+
DIGIT_FACTORIAL: dict[str, int] = {str(digit): factorial(digit) for digit in range(10)}
4539

46-
# The case of 0! is handled separately
47-
if a == 0:
48-
factorial_cache[a] = 1
49-
else:
50-
# use a temporary support variable to store the computation
51-
temporary_number = a
52-
temporary_computation = 1
5340

54-
while temporary_number > 0:
55-
temporary_computation *= temporary_number
56-
temporary_number -= 1
41+
def digit_factorial_sum(number: int) -> int:
42+
"""
43+
Function to perform the sum of the factorial of all the digits in number
5744
58-
factorial_cache[a] = temporary_computation
59-
return factorial_cache[a]
45+
>>> digit_factorial_sum(69.0)
46+
Traceback (most recent call last):
47+
...
48+
TypeError: Parameter number must be int
6049
50+
>>> digit_factorial_sum(-1)
51+
Traceback (most recent call last):
52+
...
53+
ValueError: Parameter number must be greater than or equal to 0
6154
62-
def factorial_sum(a: int) -> int:
63-
"""Function to perform the sum of the factorial
64-
of all the digits in a
55+
>>> digit_factorial_sum(0)
56+
1
6557
66-
>>> factorial_sum(69)
58+
>>> digit_factorial_sum(69)
6759
363600
6860
"""
69-
if a in factorial_sum_cache:
70-
return factorial_sum_cache[a]
71-
# Prepare a variable to hold the computation
72-
fact_sum = 0
73-
74-
""" Convert a in string to iterate on its digits
75-
convert the digit back into an int
76-
and add its factorial to fact_sum.
77-
"""
78-
for i in str(a):
79-
fact_sum += factorial(int(i))
80-
factorial_sum_cache[a] = fact_sum
81-
return fact_sum
61+
if not isinstance(number, int):
62+
raise TypeError("Parameter number must be int")
63+
64+
if number < 0:
65+
raise ValueError("Parameter number must be greater than or equal to 0")
66+
67+
# Converts number in string to iterate on its digits and adds its factorial.
68+
return sum(DIGIT_FACTORIAL[digit] for digit in str(number))
8269

8370

8471
def solution(chain_length: int = 60, number_limit: int = 1000000) -> int:
85-
"""Returns the number of numbers that produce
86-
chains with exactly 60 non repeating elements.
87-
>>> solution(10, 1000)
88-
26
8972
"""
73+
Returns the number of numbers below number_limit that produce chains with exactly
74+
chain_length non repeating elements.
9075
91-
# the counter for the chains with the exact desired length
92-
chain_counter = 0
93-
94-
for i in range(1, number_limit + 1):
76+
>>> solution(10.0, 1000)
77+
Traceback (most recent call last):
78+
...
79+
TypeError: Parameters chain_length and number_limit must be int
9580
96-
# The temporary list will contain the elements of the chain
97-
chain_set = {i}
98-
len_chain_set = 1
99-
last_chain_element = i
81+
>>> solution(10, 1000.0)
82+
Traceback (most recent call last):
83+
...
84+
TypeError: Parameters chain_length and number_limit must be int
10085
101-
# The new element of the chain
102-
new_chain_element = factorial_sum(last_chain_element)
86+
>>> solution(0, 1000)
87+
Traceback (most recent call last):
88+
...
89+
ValueError: Parameters chain_length and number_limit must be greater than 0
10390
104-
# Stop computing the chain when you find a repeating item
105-
# or the length it greater then the desired one.
91+
>>> solution(10, 0)
92+
Traceback (most recent call last):
93+
...
94+
ValueError: Parameters chain_length and number_limit must be greater than 0
10695
107-
while new_chain_element not in chain_set and len_chain_set <= chain_length:
108-
chain_set.add(new_chain_element)
96+
>>> solution(10, 1000)
97+
26
98+
"""
10999

110-
len_chain_set += 1
111-
last_chain_element = new_chain_element
112-
new_chain_element = factorial_sum(last_chain_element)
100+
if not isinstance(chain_length, int) or not isinstance(number_limit, int):
101+
raise TypeError("Parameters chain_length and number_limit must be int")
113102

114-
# If the while exited because the chain list contains the exact amount
115-
# of elements increase the counter
116-
if len_chain_set == chain_length:
117-
chain_counter += 1
103+
if chain_length <= 0 or number_limit <= 0:
104+
raise ValueError(
105+
"Parameters chain_length and number_limit must be greater than 0"
106+
)
118107

119-
return chain_counter
108+
# the counter for the chains with the exact desired length
109+
chains_counter = 0
110+
# the cached sizes of the previous chains
111+
chain_sets_lengths: dict[int, int] = {}
112+
113+
for start_chain_element in range(1, number_limit):
114+
115+
# The temporary set will contain the elements of the chain
116+
chain_set = set()
117+
chain_set_length = 0
118+
119+
# Stop computing the chain when you find a cached size, a repeating item or the
120+
# length is greater then the desired one.
121+
chain_element = start_chain_element
122+
while (
123+
chain_element not in chain_sets_lengths
124+
and chain_element not in chain_set
125+
and chain_set_length <= chain_length
126+
):
127+
chain_set.add(chain_element)
128+
chain_set_length += 1
129+
chain_element = digit_factorial_sum(chain_element)
130+
131+
if chain_element in chain_sets_lengths:
132+
chain_set_length += chain_sets_lengths[chain_element]
133+
134+
chain_sets_lengths[start_chain_element] = chain_set_length
135+
136+
# If chain contains the exact amount of elements increase the counter
137+
if chain_set_length == chain_length:
138+
chains_counter += 1
139+
140+
return chains_counter
120141

121142

122143
if __name__ == "__main__":

0 commit comments

Comments
 (0)