Improve Project Euler problem 074 solution 2 (#5803)

* Fix statement

* Improve solution

* Fix

* Add tests
This commit is contained in:
Maxim Smolskiy 2022-05-12 06:48:04 +03:00 committed by GitHub
parent 533eea5afa
commit 562cf31a9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,122 +1,143 @@
"""
Project Euler Problem 074: https://projecteuler.net/problem=74
Project Euler Problem 074: https://projecteuler.net/problem=74
Starting from any positive integer number
it is possible to attain another one summing the factorial of its digits.
The number 145 is well known for the property that the sum of the factorial of its
digits is equal to 145:
Repeating this step, we can build chains of numbers.
It is not difficult to prove that EVERY starting number
will eventually get stuck in a loop.
1! + 4! + 5! = 1 + 24 + 120 = 145
The request is to find how many numbers less than one million
produce a chain with exactly 60 non repeating items.
Perhaps less well known is 169, in that it produces the longest chain of numbers that
link back to 169; it turns out that there are only three such loops that exist:
Solution approach:
This solution simply consists in a loop that generates
the chains of non repeating items.
The generation of the chain stops before a repeating item
or if the size of the chain is greater then the desired one.
After generating each chain, the length is checked and the
counter increases.
169 363601 1454 169
871 45361 871
872 45362 872
It is not difficult to prove that EVERY starting number will eventually get stuck in a
loop. For example,
69 363600 1454 169 363601 ( 1454)
78 45360 871 45361 ( 871)
540 145 ( 145)
Starting with 69 produces a chain of five non-repeating terms, but the longest
non-repeating chain with a starting number below one million is sixty terms.
How many chains, with a starting number below one million, contain exactly sixty
non-repeating terms?
Solution approach:
This solution simply consists in a loop that generates the chains of non repeating
items using the cached sizes of the previous chains.
The generation of the chain stops before a repeating item or if the size of the chain
is greater then the desired one.
After generating each chain, the length is checked and the counter increases.
"""
from math import factorial
factorial_cache: dict[int, int] = {}
factorial_sum_cache: dict[int, int] = {}
DIGIT_FACTORIAL: dict[str, int] = {str(digit): factorial(digit) for digit in range(10)}
def factorial(a: int) -> int:
"""Returns the factorial of the input a
>>> factorial(5)
120
>>> factorial(6)
720
>>> factorial(0)
1
def digit_factorial_sum(number: int) -> int:
"""
Function to perform the sum of the factorial of all the digits in number
# The factorial function is not defined for negative numbers
if a < 0:
raise ValueError("Invalid negative input!", a)
>>> digit_factorial_sum(69.0)
Traceback (most recent call last):
...
TypeError: Parameter number must be int
if a in factorial_cache:
return factorial_cache[a]
>>> digit_factorial_sum(-1)
Traceback (most recent call last):
...
ValueError: Parameter number must be greater than or equal to 0
# The case of 0! is handled separately
if a == 0:
factorial_cache[a] = 1
else:
# use a temporary support variable to store the computation
temporary_number = a
temporary_computation = 1
>>> digit_factorial_sum(0)
1
while temporary_number > 0:
temporary_computation *= temporary_number
temporary_number -= 1
factorial_cache[a] = temporary_computation
return factorial_cache[a]
def factorial_sum(a: int) -> int:
"""Function to perform the sum of the factorial
of all the digits in a
>>> factorial_sum(69)
>>> digit_factorial_sum(69)
363600
"""
if a in factorial_sum_cache:
return factorial_sum_cache[a]
# Prepare a variable to hold the computation
fact_sum = 0
if not isinstance(number, int):
raise TypeError("Parameter number must be int")
""" Convert a in string to iterate on its digits
convert the digit back into an int
and add its factorial to fact_sum.
"""
for i in str(a):
fact_sum += factorial(int(i))
factorial_sum_cache[a] = fact_sum
return fact_sum
if number < 0:
raise ValueError("Parameter number must be greater than or equal to 0")
# Converts number in string to iterate on its digits and adds its factorial.
return sum(DIGIT_FACTORIAL[digit] for digit in str(number))
def solution(chain_length: int = 60, number_limit: int = 1000000) -> int:
"""Returns the number of numbers that produce
chains with exactly 60 non repeating elements.
"""
Returns the number of numbers below number_limit that produce chains with exactly
chain_length non repeating elements.
>>> solution(10.0, 1000)
Traceback (most recent call last):
...
TypeError: Parameters chain_length and number_limit must be int
>>> solution(10, 1000.0)
Traceback (most recent call last):
...
TypeError: Parameters chain_length and number_limit must be int
>>> solution(0, 1000)
Traceback (most recent call last):
...
ValueError: Parameters chain_length and number_limit must be greater than 0
>>> solution(10, 0)
Traceback (most recent call last):
...
ValueError: Parameters chain_length and number_limit must be greater than 0
>>> solution(10, 1000)
26
"""
if not isinstance(chain_length, int) or not isinstance(number_limit, int):
raise TypeError("Parameters chain_length and number_limit must be int")
if chain_length <= 0 or number_limit <= 0:
raise ValueError(
"Parameters chain_length and number_limit must be greater than 0"
)
# the counter for the chains with the exact desired length
chain_counter = 0
chains_counter = 0
# the cached sizes of the previous chains
chain_sets_lengths: dict[int, int] = {}
for i in range(1, number_limit + 1):
for start_chain_element in range(1, number_limit):
# The temporary list will contain the elements of the chain
chain_set = {i}
len_chain_set = 1
last_chain_element = i
# The temporary set will contain the elements of the chain
chain_set = set()
chain_set_length = 0
# The new element of the chain
new_chain_element = factorial_sum(last_chain_element)
# Stop computing the chain when you find a cached size, a repeating item or the
# length is greater then the desired one.
chain_element = start_chain_element
while (
chain_element not in chain_sets_lengths
and chain_element not in chain_set
and chain_set_length <= chain_length
):
chain_set.add(chain_element)
chain_set_length += 1
chain_element = digit_factorial_sum(chain_element)
# Stop computing the chain when you find a repeating item
# or the length it greater then the desired one.
if chain_element in chain_sets_lengths:
chain_set_length += chain_sets_lengths[chain_element]
while new_chain_element not in chain_set and len_chain_set <= chain_length:
chain_set.add(new_chain_element)
chain_sets_lengths[start_chain_element] = chain_set_length
len_chain_set += 1
last_chain_element = new_chain_element
new_chain_element = factorial_sum(last_chain_element)
# If chain contains the exact amount of elements increase the counter
if chain_set_length == chain_length:
chains_counter += 1
# If the while exited because the chain list contains the exact amount
# of elements increase the counter
if len_chain_set == chain_length:
chain_counter += 1
return chain_counter
return chains_counter
if __name__ == "__main__":