From ccb0928cc005430cbf23f74a2b7990cc45998b29 Mon Sep 17 00:00:00 2001 From: MaximSmolskiy Date: Wed, 10 Nov 2021 09:30:06 +0300 Subject: [PATCH 1/4] Fix statement --- project_euler/problem_074/sol2.py | 45 ++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/project_euler/problem_074/sol2.py b/project_euler/problem_074/sol2.py index 55e67c6b98dd..f656752332f6 100644 --- a/project_euler/problem_074/sol2.py +++ b/project_euler/problem_074/sol2.py @@ -1,23 +1,38 @@ """ - 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. +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. """ factorial_cache: dict[int, int] = {} From 765090736ceed8c0531a36563ecdf2db3c437be4 Mon Sep 17 00:00:00 2001 From: MaximSmolskiy Date: Wed, 10 Nov 2021 10:34:14 +0300 Subject: [PATCH 2/4] Improve solution --- project_euler/problem_074/sol2.py | 137 +++++++++++------------------- 1 file changed, 48 insertions(+), 89 deletions(-) diff --git a/project_euler/problem_074/sol2.py b/project_euler/problem_074/sol2.py index f656752332f6..0c09a8fd454a 100644 --- a/project_euler/problem_074/sol2.py +++ b/project_euler/problem_074/sol2.py @@ -27,111 +27,70 @@ non-repeating terms? 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. +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) - - if a in factorial_cache: - return factorial_cache[a] - - # 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 - - 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 - - """ 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 + # 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, 1000) 26 """ # the counter for the chains with the exact desired length - chain_counter = 0 - - for i in range(1, number_limit + 1): - - # The temporary list will contain the elements of the chain - chain_set = {i} - len_chain_set = 1 - last_chain_element = i - - # The new element of the chain - new_chain_element = factorial_sum(last_chain_element) - - # Stop computing the chain when you find a repeating item - # or the length it greater then the desired one. - - while new_chain_element not in chain_set and len_chain_set <= chain_length: - chain_set.add(new_chain_element) - - len_chain_set += 1 - last_chain_element = new_chain_element - new_chain_element = factorial_sum(last_chain_element) - - # 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 + chains_counter = 0 + # the cached sizes of the previous chains + chain_sets_lengths = {} + + for start_chain_element in range(1, number_limit): + + # The temporary set will contain the elements of the chain + chain_set = set() + chain_set_length = 0 + + # 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) + + if chain_element in chain_sets_lengths: + chain_set_length += chain_sets_lengths[chain_element] + + chain_sets_lengths[start_chain_element] = chain_set_length + + # If chain contains the exact amount of elements increase the counter + if chain_set_length == chain_length: + chains_counter += 1 + + return chains_counter if __name__ == "__main__": From ab84d57d8ffee8cba3ff2f6e4d59c08a09964d9f Mon Sep 17 00:00:00 2001 From: MaximSmolskiy Date: Wed, 10 Nov 2021 10:41:39 +0300 Subject: [PATCH 3/4] Fix --- project_euler/problem_074/sol2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project_euler/problem_074/sol2.py b/project_euler/problem_074/sol2.py index 0c09a8fd454a..d70e90f0d7c1 100644 --- a/project_euler/problem_074/sol2.py +++ b/project_euler/problem_074/sol2.py @@ -61,7 +61,7 @@ def solution(chain_length: int = 60, number_limit: int = 1000000) -> int: # the counter for the chains with the exact desired length chains_counter = 0 # the cached sizes of the previous chains - chain_sets_lengths = {} + chain_sets_lengths: dict[int, int] = {} for start_chain_element in range(1, number_limit): From 3f8859898bc5a239b1280afd88f1354f4c983b50 Mon Sep 17 00:00:00 2001 From: MaximSmolskiy Date: Sat, 13 Nov 2021 18:35:59 +0300 Subject: [PATCH 4/4] Add tests --- project_euler/problem_074/sol2.py | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/project_euler/problem_074/sol2.py b/project_euler/problem_074/sol2.py index d70e90f0d7c1..d76bb014d629 100644 --- a/project_euler/problem_074/sol2.py +++ b/project_euler/problem_074/sol2.py @@ -42,9 +42,28 @@ def digit_factorial_sum(number: int) -> int: """ Function to perform the sum of the factorial of all the digits in number + >>> digit_factorial_sum(69.0) + Traceback (most recent call last): + ... + TypeError: Parameter number must be int + + >>> digit_factorial_sum(-1) + Traceback (most recent call last): + ... + ValueError: Parameter number must be greater than or equal to 0 + + >>> digit_factorial_sum(0) + 1 + >>> digit_factorial_sum(69) 363600 """ + if not isinstance(number, int): + raise TypeError("Parameter number must be int") + + 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)) @@ -54,10 +73,38 @@ def solution(chain_length: int = 60, number_limit: int = 1000000) -> int: 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 chains_counter = 0 # the cached sizes of the previous chains