From ab153b2de6d35764161f50d985dc91d08a79d255 Mon Sep 17 00:00:00 2001 From: Akshar Goyal Date: Mon, 13 Oct 2025 18:14:42 +0000 Subject: [PATCH 1/7] feat(project_euler): add solution to problem 60 --- project_euler/problem_060/__init__.py | 0 project_euler/problem_060/sol1.py | 241 ++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 project_euler/problem_060/__init__.py create mode 100644 project_euler/problem_060/sol1.py diff --git a/project_euler/problem_060/__init__.py b/project_euler/problem_060/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_060/sol1.py b/project_euler/problem_060/sol1.py new file mode 100644 index 000000000000..122d15b3622b --- /dev/null +++ b/project_euler/problem_060/sol1.py @@ -0,0 +1,241 @@ +""" +Project Euler Problem 60: https://projecteuler.net/problem=60 + +# Problem Statement: + +The primes 3, 7, 109, and 673 are quite remarkable. By taking any two primes +and concatenating them in any order the result will always be prime. +For example, taking 7 and 109, both 7109 and 1097 are prime. +The sum of these four primes, 792, represents the lowest sum for a set of four primes +with this property. +Find the lowest sum for a set of five primes for which any two primes concatenate +to produce another prime. + +# Solution Explanation: + +The brute force approach would be to check all combinations of 5 primes and check +if they satisfy the concatenation property. However, this is computationally +expensive. Instead, we can use a backtracking approach to build sets of primes +that satisfy the concatenation property. We can further optimize by using property +of divisibility by 3 to eliminate certain candidates and memoization to avoid +redundant prime checks. +Throughout the code, we have used a parameter flag to indicate whether +we are working with primes that are congruent to 1 or 2 modulo 3. +This helps in reducing the search space. + +## Eliminating candidates using divisibility by 3: +Consider any 2 primes p1 and p2 that are not divisible by 3. If p1 divided by 3 +gives a remainder of 1 and p2 divided by 3 gives a remainder of 2, then +the concatenated number p1p2 will be divisible by 3 and hence not prime. +This can be easily proven using the property of modular arithmetic. + Consider p1 ≡ 1 (mod 3) and p2 ≡ 2 (mod 3). Define a1 = p1, b1 = 1, a2 = p2, b2 = 2. + concat(p1, p2) = (p1 * 10^k + p2) where k is the number of digits in p2. + Now, (p1 * 10^k + p2) mod 3 = ((p1 * 10^k) + p2) mod 3 + As 10^k mod 3 = 1, we have (p1 * 1 + p2) mod 3 (ka mod 3 = kb mod 3) + Which implies (p1 + p2) mod 3 = (1 + 2) mod 3 = 0 (a1 + a2 mod 3 = b1 + b2 mod 3) + +Thus, we can eliminate such pairs from our search space and reach the solution faster. +The solution uses this property to divide the primes into two lists based on their +remainder when divided by 3. This way, we only need to check combinations within +either list, reducing the number of checks significantly. + +## Memoization: +We can use a dictionary to store the results of prime checks for concatenated numbers. +This way, if we encounter the same concatenated number again, we can simply look up +the result instead of recalculating it. + +## Backtracking: +We can use a recursive function to build sets of primes. Starting with an empty set, +we can add primes one by one, checking at each step if the current set satisfies +the concatenation property. If it does, we can continue adding more primes. +If we reach a set of 5 primes, we can check if their sum is the lowest + +References: +- [Modular Arithmetic Explanation](https://en.wikipedia.org/wiki/Modular_arithmetic) +- [Project Euler Forum Discussion](https://projecteuler.net/problem=60) +- [Prime Checking Optimization](https://en.wikipedia.org/wiki/Primality_test) +- [Backtracking Algorithm](https://en.wikipedia.org/wiki/Backtracking) +""" + +from functools import cache + +prime_mod_3_is_1_list: list[int] = [3, 7, 13, 19] +prime_mod_3_is_2_list: list[int] = [3, 5, 11, 17] + +prime_pairs: dict[tuple, bool] = {} + + +@cache +def is_prime(num: int) -> bool: + """ + Efficient primality check using 6k ± 1 optimization. + + >>> is_prime(0) + False + >>> is_prime(1) + False + >>> is_prime(2) + True + >>> is_prime(3) + True + >>> is_prime(77) + False + >>> is_prime(673) + True + >>> is_prime(1097) + True + >>> is_prime(7109) + True + """ + + if num < 2: + return False + if num in (2, 3): + return True + if num % 2 == 0 or num % 3 == 0: + return False + # Check divisibility up to sqrt(num) + n_sqrt = int(num**0.5) + for i in range(5, n_sqrt + 1, 6): + if num % i == 0 or num % (i + 2) == 0: + return False + return True + + +def sum_digits(num: int) -> int: + """ + Returns the sum of digits of num. If the sum is greater than 10, + it recursively sums the digits of the result until a single digit is obtained. + + >>> sum_digits(-18) + Traceback (most recent call last): + ... + ValueError: num must be non-negative + >>> sum_digits(0) + 0 + >>> sum_digits(5) + 5 + >>> sum_digits(79) + 7 + >>> sum_digits(999) + 9 + """ + if num < 0: + raise ValueError("num must be non-negative") + if num < 10: + return num + return sum_digits(sum(map(int, str(num)))) + + +def is_concat(num1: int, num2: int) -> bool: + """ + Check if concatenations of num1+num2 and num2+num1 are both prime. + Uses memoization to store previously computed results in prime_pairs dictionary. + Effects: Updates the prime_pairs dictionary with the result. + Only stores (min(num1, num2), max(num1, num2)) as key to avoid duplicates. + + >>> is_concat(3, 7) + True + >>> is_concat(1, 6) + False + >>> is_concat(7, 109) + True + >>> is_concat(13, 31) + False + """ + if num1 > num2: + num1, num2 = num2, num1 + key = (num1, num2) + if key in prime_pairs: + return prime_pairs[key] + concat1 = int(f"{num1}{num2}") + concat2 = int(f"{num2}{num1}") + result = is_prime(concat1) and is_prime(concat2) + prime_pairs[key] = result + return result + + +def add_prime(primes: list[int]) -> list[int]: + """ + Add a new prime number to the input list of primes based on its modulo 3 value. + Effects: Modifies the input list by appending a new prime number. + + >>> add_prime([3, 7, 13, 19]) + [3, 7, 13, 19, 31] + >>> add_prime([3, 5, 11, 17]) + [3, 5, 11, 17, 23] + >>> add_prime([3, 7, 13, 19, 31]) + [3, 7, 13, 19, 31, 37] + """ + + next_num = primes[-1] + 3 # using modular arithmetic to get similar primes + while not is_prime(next_num): + next_num += 3 + primes.append(next_num) + return primes + + +def generate_primes(n: int, flag: int = 1) -> list[int]: + """ + Ensure we have at least n primes in the selected list. + + >>> generate_primes(5, 1) + [3, 7, 13, 19, 31] + >>> generate_primes(5, 2) + [3, 5, 11, 17, 23] + """ + primes = prime_mod_3_is_1_list if flag == 1 else prime_mod_3_is_2_list + while len(primes) < n: + primes = add_prime(primes) + return primes + + +def solution( + target_size: int = 5, prime_limit: int = 1000, flag: int = 1 +) -> int | None: + """ + Search for a set of primes with the concat-prime property. + Returns the sum of the lowest such set found else returns None. + + >>> solution(3, 100, None) + Traceback (most recent call last): + ... + ValueError: flag must be either 1 or 2 + >>> solution(4, 100, 1) + 792 + >>> solution(3, 100, 2) + 715 + >>> solution(5, 1000, 1) + 26033 + """ + if flag not in (1, 2): + raise ValueError("flag must be either 1 or 2") + primes = generate_primes(prime_limit, flag) + + def search(chain): + """ + Recursive backtracking search to find a valid set of primes. + A threshold is used to ensure we don't exceed the smallest sum. + Returns the valid set if found, else None. + """ + if len(chain) == target_size: + return chain + for p in primes: + if p <= chain[-1]: + continue + if all(is_concat(p, c) for c in chain): + result = search((*chain, p)) + if result: + return result + return None + + for _, p in enumerate(primes): + result = search((p,)) + if result and len(result) == target_size: + return sum(result) + + return None # No valid set found + + +if __name__ == "__main__": + print(f"{solution() = }") From 74f44ff8863863a1c8a87129063256fd76e056cc Mon Sep 17 00:00:00 2001 From: Akshar Goyal Date: Mon, 13 Oct 2025 18:24:37 +0000 Subject: [PATCH 2/7] fix(project_euler): fix var name and type hints --- project_euler/problem_060/sol1.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project_euler/problem_060/sol1.py b/project_euler/problem_060/sol1.py index 122d15b3622b..6bc168afd253 100644 --- a/project_euler/problem_060/sol1.py +++ b/project_euler/problem_060/sol1.py @@ -175,9 +175,9 @@ def add_prime(primes: list[int]) -> list[int]: return primes -def generate_primes(n: int, flag: int = 1) -> list[int]: +def generate_primes(num_primes: int, flag: int = 1) -> list[int]: """ - Ensure we have at least n primes in the selected list. + Generates a list of the first num_primes primes based on their modulo 3 value. >>> generate_primes(5, 1) [3, 7, 13, 19, 31] @@ -185,7 +185,7 @@ def generate_primes(n: int, flag: int = 1) -> list[int]: [3, 5, 11, 17, 23] """ primes = prime_mod_3_is_1_list if flag == 1 else prime_mod_3_is_2_list - while len(primes) < n: + while len(primes) < num_primes: primes = add_prime(primes) return primes @@ -212,7 +212,7 @@ def solution( raise ValueError("flag must be either 1 or 2") primes = generate_primes(prime_limit, flag) - def search(chain): + def search(chain: tuple) -> tuple[int, ...] | None: """ Recursive backtracking search to find a valid set of primes. A threshold is used to ensure we don't exceed the smallest sum. From e6af7f7f564d455b76aeca895bf20d7d64e85075 Mon Sep 17 00:00:00 2001 From: Akshar Goyal Date: Mon, 13 Oct 2025 18:31:16 +0000 Subject: [PATCH 3/7] chore(project_euler): update directory to include problem 60 solution --- DIRECTORY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 6249b75c4231..74d68f0d495c 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -1075,6 +1075,8 @@ * [Sol1](project_euler/problem_058/sol1.py) * Problem 059 * [Sol1](project_euler/problem_059/sol1.py) + * Problem 060 + * [Sol1](project_euler/problem_060/sol1.py) * Problem 062 * [Sol1](project_euler/problem_062/sol1.py) * Problem 063 From 8fe31f79dab426e644875f3ce70ea5d04ffc1700 Mon Sep 17 00:00:00 2001 From: Akshar Goyal Date: Mon, 13 Oct 2025 18:36:55 +0000 Subject: [PATCH 4/7] chore(directory): update DIRECTORY.md to keep it about current files --- DIRECTORY.md | 1 - 1 file changed, 1 deletion(-) diff --git a/DIRECTORY.md b/DIRECTORY.md index 74d68f0d495c..32104a2a12b8 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -195,7 +195,6 @@ * [Permutations](data_structures/arrays/permutations.py) * [Prefix Sum](data_structures/arrays/prefix_sum.py) * [Product Sum](data_structures/arrays/product_sum.py) - * [Rotate Array](data_structures/arrays/rotate_array.py) * [Sparse Table](data_structures/arrays/sparse_table.py) * [Sudoku Solver](data_structures/arrays/sudoku_solver.py) * Binary Tree From 3480740f867065cb20ffa475bfcdb0e734686f8f Mon Sep 17 00:00:00 2001 From: Akshar Goyal Date: Mon, 13 Oct 2025 18:38:02 +0000 Subject: [PATCH 5/7] fix(project_euler): add doctest to search --- project_euler/problem_060/sol1.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/project_euler/problem_060/sol1.py b/project_euler/problem_060/sol1.py index 6bc168afd253..bceeaafc4f73 100644 --- a/project_euler/problem_060/sol1.py +++ b/project_euler/problem_060/sol1.py @@ -217,6 +217,11 @@ def search(chain: tuple) -> tuple[int, ...] | None: Recursive backtracking search to find a valid set of primes. A threshold is used to ensure we don't exceed the smallest sum. Returns the valid set if found, else None. + + >>> search((3,)) + (3, 7, 109, 673) + >>> search((7,)) + (7, 109, 673, 3) """ if len(chain) == target_size: return chain From 2d02770f57dd9d7eb14b15675d681e9e51b82454 Mon Sep 17 00:00:00 2001 From: AksharGoyal Date: Mon, 13 Oct 2025 18:39:30 +0000 Subject: [PATCH 6/7] updating DIRECTORY.md --- DIRECTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 32104a2a12b8..74d68f0d495c 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -195,6 +195,7 @@ * [Permutations](data_structures/arrays/permutations.py) * [Prefix Sum](data_structures/arrays/prefix_sum.py) * [Product Sum](data_structures/arrays/product_sum.py) + * [Rotate Array](data_structures/arrays/rotate_array.py) * [Sparse Table](data_structures/arrays/sparse_table.py) * [Sudoku Solver](data_structures/arrays/sudoku_solver.py) * Binary Tree From d2d87db186b051b30503c30b04d412adf5d3f24d Mon Sep 17 00:00:00 2001 From: AksharGoyal Date: Wed, 15 Oct 2025 05:45:01 +0000 Subject: [PATCH 7/7] updating DIRECTORY.md --- DIRECTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 74d68f0d495c..890c274c880e 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -624,6 +624,7 @@ * [Sequential Minimum Optimization](machine_learning/sequential_minimum_optimization.py) * [Similarity Search](machine_learning/similarity_search.py) * [Support Vector Machines](machine_learning/support_vector_machines.py) + * [T Stochastic Neighbour Embedding](machine_learning/t_stochastic_neighbour_embedding.py) * [Word Frequency Functions](machine_learning/word_frequency_functions.py) * [Xgboost Classifier](machine_learning/xgboost_classifier.py) * [Xgboost Regressor](machine_learning/xgboost_regressor.py)