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