Skip to content

Commit ab153b2

Browse files
committed
feat(project_euler): add solution to problem 60
1 parent 709c18e commit ab153b2

File tree

2 files changed

+241
-0
lines changed

2 files changed

+241
-0
lines changed

project_euler/problem_060/__init__.py

Whitespace-only changes.

project_euler/problem_060/sol1.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
"""
2+
Project Euler Problem 60: https://projecteuler.net/problem=60
3+
4+
# Problem Statement:
5+
6+
The primes 3, 7, 109, and 673 are quite remarkable. By taking any two primes
7+
and concatenating them in any order the result will always be prime.
8+
For example, taking 7 and 109, both 7109 and 1097 are prime.
9+
The sum of these four primes, 792, represents the lowest sum for a set of four primes
10+
with this property.
11+
Find the lowest sum for a set of five primes for which any two primes concatenate
12+
to produce another prime.
13+
14+
# Solution Explanation:
15+
16+
The brute force approach would be to check all combinations of 5 primes and check
17+
if they satisfy the concatenation property. However, this is computationally
18+
expensive. Instead, we can use a backtracking approach to build sets of primes
19+
that satisfy the concatenation property. We can further optimize by using property
20+
of divisibility by 3 to eliminate certain candidates and memoization to avoid
21+
redundant prime checks.
22+
Throughout the code, we have used a parameter flag to indicate whether
23+
we are working with primes that are congruent to 1 or 2 modulo 3.
24+
This helps in reducing the search space.
25+
26+
## Eliminating candidates using divisibility by 3:
27+
Consider any 2 primes p1 and p2 that are not divisible by 3. If p1 divided by 3
28+
gives a remainder of 1 and p2 divided by 3 gives a remainder of 2, then
29+
the concatenated number p1p2 will be divisible by 3 and hence not prime.
30+
This can be easily proven using the property of modular arithmetic.
31+
Consider p1 ≡ 1 (mod 3) and p2 ≡ 2 (mod 3). Define a1 = p1, b1 = 1, a2 = p2, b2 = 2.
32+
concat(p1, p2) = (p1 * 10^k + p2) where k is the number of digits in p2.
33+
Now, (p1 * 10^k + p2) mod 3 = ((p1 * 10^k) + p2) mod 3
34+
As 10^k mod 3 = 1, we have (p1 * 1 + p2) mod 3 (ka mod 3 = kb mod 3)
35+
Which implies (p1 + p2) mod 3 = (1 + 2) mod 3 = 0 (a1 + a2 mod 3 = b1 + b2 mod 3)
36+
37+
Thus, we can eliminate such pairs from our search space and reach the solution faster.
38+
The solution uses this property to divide the primes into two lists based on their
39+
remainder when divided by 3. This way, we only need to check combinations within
40+
either list, reducing the number of checks significantly.
41+
42+
## Memoization:
43+
We can use a dictionary to store the results of prime checks for concatenated numbers.
44+
This way, if we encounter the same concatenated number again, we can simply look up
45+
the result instead of recalculating it.
46+
47+
## Backtracking:
48+
We can use a recursive function to build sets of primes. Starting with an empty set,
49+
we can add primes one by one, checking at each step if the current set satisfies
50+
the concatenation property. If it does, we can continue adding more primes.
51+
If we reach a set of 5 primes, we can check if their sum is the lowest
52+
53+
References:
54+
- [Modular Arithmetic Explanation](https://en.wikipedia.org/wiki/Modular_arithmetic)
55+
- [Project Euler Forum Discussion](https://projecteuler.net/problem=60)
56+
- [Prime Checking Optimization](https://en.wikipedia.org/wiki/Primality_test)
57+
- [Backtracking Algorithm](https://en.wikipedia.org/wiki/Backtracking)
58+
"""
59+
60+
from functools import cache
61+
62+
prime_mod_3_is_1_list: list[int] = [3, 7, 13, 19]
63+
prime_mod_3_is_2_list: list[int] = [3, 5, 11, 17]
64+
65+
prime_pairs: dict[tuple, bool] = {}
66+
67+
68+
@cache
69+
def is_prime(num: int) -> bool:
70+
"""
71+
Efficient primality check using 6k ± 1 optimization.
72+
73+
>>> is_prime(0)
74+
False
75+
>>> is_prime(1)
76+
False
77+
>>> is_prime(2)
78+
True
79+
>>> is_prime(3)
80+
True
81+
>>> is_prime(77)
82+
False
83+
>>> is_prime(673)
84+
True
85+
>>> is_prime(1097)
86+
True
87+
>>> is_prime(7109)
88+
True
89+
"""
90+
91+
if num < 2:
92+
return False
93+
if num in (2, 3):
94+
return True
95+
if num % 2 == 0 or num % 3 == 0:
96+
return False
97+
# Check divisibility up to sqrt(num)
98+
n_sqrt = int(num**0.5)
99+
for i in range(5, n_sqrt + 1, 6):
100+
if num % i == 0 or num % (i + 2) == 0:
101+
return False
102+
return True
103+
104+
105+
def sum_digits(num: int) -> int:
106+
"""
107+
Returns the sum of digits of num. If the sum is greater than 10,
108+
it recursively sums the digits of the result until a single digit is obtained.
109+
110+
>>> sum_digits(-18)
111+
Traceback (most recent call last):
112+
...
113+
ValueError: num must be non-negative
114+
>>> sum_digits(0)
115+
0
116+
>>> sum_digits(5)
117+
5
118+
>>> sum_digits(79)
119+
7
120+
>>> sum_digits(999)
121+
9
122+
"""
123+
if num < 0:
124+
raise ValueError("num must be non-negative")
125+
if num < 10:
126+
return num
127+
return sum_digits(sum(map(int, str(num))))
128+
129+
130+
def is_concat(num1: int, num2: int) -> bool:
131+
"""
132+
Check if concatenations of num1+num2 and num2+num1 are both prime.
133+
Uses memoization to store previously computed results in prime_pairs dictionary.
134+
Effects: Updates the prime_pairs dictionary with the result.
135+
Only stores (min(num1, num2), max(num1, num2)) as key to avoid duplicates.
136+
137+
>>> is_concat(3, 7)
138+
True
139+
>>> is_concat(1, 6)
140+
False
141+
>>> is_concat(7, 109)
142+
True
143+
>>> is_concat(13, 31)
144+
False
145+
"""
146+
if num1 > num2:
147+
num1, num2 = num2, num1
148+
key = (num1, num2)
149+
if key in prime_pairs:
150+
return prime_pairs[key]
151+
concat1 = int(f"{num1}{num2}")
152+
concat2 = int(f"{num2}{num1}")
153+
result = is_prime(concat1) and is_prime(concat2)
154+
prime_pairs[key] = result
155+
return result
156+
157+
158+
def add_prime(primes: list[int]) -> list[int]:
159+
"""
160+
Add a new prime number to the input list of primes based on its modulo 3 value.
161+
Effects: Modifies the input list by appending a new prime number.
162+
163+
>>> add_prime([3, 7, 13, 19])
164+
[3, 7, 13, 19, 31]
165+
>>> add_prime([3, 5, 11, 17])
166+
[3, 5, 11, 17, 23]
167+
>>> add_prime([3, 7, 13, 19, 31])
168+
[3, 7, 13, 19, 31, 37]
169+
"""
170+
171+
next_num = primes[-1] + 3 # using modular arithmetic to get similar primes
172+
while not is_prime(next_num):
173+
next_num += 3
174+
primes.append(next_num)
175+
return primes
176+
177+
178+
def generate_primes(n: int, flag: int = 1) -> list[int]:
179+
"""
180+
Ensure we have at least n primes in the selected list.
181+
182+
>>> generate_primes(5, 1)
183+
[3, 7, 13, 19, 31]
184+
>>> generate_primes(5, 2)
185+
[3, 5, 11, 17, 23]
186+
"""
187+
primes = prime_mod_3_is_1_list if flag == 1 else prime_mod_3_is_2_list
188+
while len(primes) < n:
189+
primes = add_prime(primes)
190+
return primes
191+
192+
193+
def solution(
194+
target_size: int = 5, prime_limit: int = 1000, flag: int = 1
195+
) -> int | None:
196+
"""
197+
Search for a set of primes with the concat-prime property.
198+
Returns the sum of the lowest such set found else returns None.
199+
200+
>>> solution(3, 100, None)
201+
Traceback (most recent call last):
202+
...
203+
ValueError: flag must be either 1 or 2
204+
>>> solution(4, 100, 1)
205+
792
206+
>>> solution(3, 100, 2)
207+
715
208+
>>> solution(5, 1000, 1)
209+
26033
210+
"""
211+
if flag not in (1, 2):
212+
raise ValueError("flag must be either 1 or 2")
213+
primes = generate_primes(prime_limit, flag)
214+
215+
def search(chain):
216+
"""
217+
Recursive backtracking search to find a valid set of primes.
218+
A threshold is used to ensure we don't exceed the smallest sum.
219+
Returns the valid set if found, else None.
220+
"""
221+
if len(chain) == target_size:
222+
return chain
223+
for p in primes:
224+
if p <= chain[-1]:
225+
continue
226+
if all(is_concat(p, c) for c in chain):
227+
result = search((*chain, p))
228+
if result:
229+
return result
230+
return None
231+
232+
for _, p in enumerate(primes):
233+
result = search((p,))
234+
if result and len(result) == target_size:
235+
return sum(result)
236+
237+
return None # No valid set found
238+
239+
240+
if __name__ == "__main__":
241+
print(f"{solution() = }")

0 commit comments

Comments
 (0)