Skip to content

Commit c4a803d

Browse files
committed
Lesson 11 sieve of eratosthenes
1 parent 526abd5 commit c4a803d

2 files changed

Lines changed: 169 additions & 0 deletions

File tree

11_count_non_divisible.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""https://app.codility.com/programmers/lessons/11-sieve_of_eratosthenes/count_non_divisible/"""
2+
3+
from math import sqrt, floor
4+
from collections import Counter
5+
6+
# Time Complexity:
7+
# Precompute freqency count of each element in A = O(N)
8+
# Each element in A in range [1, 100_000], let M be the max element in A
9+
# Outer loop: iterate over M counts = O(M)
10+
# Inner loop: for each element iterate over possible divisors = O(sqrt(M))
11+
# Return result for each element in A = O(N)
12+
# Overall: O(2 * N + M * sqrt(M)) = O(N + M * sqrt(M))
13+
14+
def solution_a(a: list[int]) -> list[int]:
15+
"""
16+
For each element in array A, count the number of elements
17+
of A (including self) that don't divide the element.
18+
19+
First attempt: Directly count divisors for each unique element.
20+
Codility score: 88%, failing performance tests
21+
"""
22+
23+
count_lookup = Counter(a)
24+
non_divisible_count = Counter()
25+
n = len(a)
26+
for element in count_lookup:
27+
divisor_count = 0
28+
for divisor in range(1, floor(sqrt(element)) + 1):
29+
q, r = divmod(element, divisor)
30+
if r == 0:
31+
divisor_count += count_lookup[divisor]
32+
if q != divisor:
33+
divisor_count += count_lookup[q]
34+
# Non-divisors can be found implicitly through divisors
35+
num_non_divisor = n - divisor_count
36+
non_divisible_count[element] = num_non_divisor
37+
38+
return [non_divisible_count[a[i]] for i in range(n)]
39+
40+
# Time Complexity:
41+
# Precompute freqency count of each element in A = O(N)
42+
# Each element in A in range [1, 100_000], let M be the max element in A
43+
# Outer loop: iterate over M counts = O(M)
44+
45+
# Inner loop: When M is 1, does M/1 work. When M is 2, does M/2 work.
46+
# Across all M iterations, work done is:
47+
# M/1 + M/2 + M/3 + ... + M/M = M * (1 + 1/2 + 1/3 + ... + 1/M).
48+
# The bracketed terms are approximated by the harmonic series which is O(log M),
49+
# so the inner loop does total work = O(M log M).
50+
# Overall: O(N + M + M log M) = O(N + M log M)
51+
52+
def solution_b(a: list[int]) -> list[int]:
53+
"""Optimised solution using the sieve of eratosthenes."""
54+
if not a:
55+
return []
56+
57+
n = len(a)
58+
m = max(a)
59+
60+
# Freq count of each number in A
61+
counts = [0] * (m + 1)
62+
for num in a:
63+
counts[num] += 1
64+
65+
# Number of divisors for each number in A
66+
divisor_counts = [0] * (m + 1)
67+
68+
for divisor in range(1, m + 1):
69+
if counts[divisor] > 0:
70+
k = divisor
71+
while k <= m:
72+
divisor_counts[k] += counts[divisor]
73+
k += divisor
74+
75+
return [n - divisor_counts[num] for num in a]

11_count_semiprimes.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""https://app.codility.com/programmers/lessons/11-sieve_of_eratosthenes/count_semiprimes/"""
2+
3+
from math import floor, sqrt
4+
5+
# Time Complexity:
6+
# Sieve of Eratosthenes to find all primes up to N = O(Nlog(logN))
7+
# For each number up to N, check all possible divisors = O(N*sqrt(N))
8+
# For M queries, return result in O(M)
9+
# Overall: O(Nlog(logN) + Nsqrt(N) + M) = O(Nsqrt(N) + M)
10+
11+
def solution_a(n: int, p: list[int], q: list[int]) -> list[int]:
12+
"""
13+
P, Q of lengths M represent M queries.
14+
For each query, find the number of semiprimes within the inclusive range [P[K], Q[K]].
15+
A semiprime is the product of two prime numbers.
16+
E.g. 4, 6, 9, 10, 14 .. are semiprimes
17+
N is the maximum value in P or Q.
18+
Compute and return the queries
19+
20+
This solution passes 100% on Codility tests. The detected TC is O(N * log(log(N)) + M),
21+
which is not the TC we expected: O(Nsqrt(N) + M)
22+
"""
23+
24+
# sieve[i] is True if i is prime
25+
sieve = [True] * (n + 1)
26+
sieve[0] = sieve[1] = False
27+
28+
# Multiples of the form k * i where k < i are already marked by smaller primes.
29+
# E.g. for i = 5, multiples 5*2=10, 5*3=15, 5*4=20 are already marked by 2 and 3.
30+
# Therefore, start from i*i
31+
32+
i = 2
33+
while i * i <= n:
34+
if sieve[i]:
35+
k = i * i
36+
while k <= n:
37+
sieve[k] = False
38+
k += i
39+
i += 1
40+
41+
semiprime_count = 0
42+
# prefix[i] is the number of semiprimes up to and including number i
43+
prefix = []
44+
# For each number check all possible divisors
45+
for num in range(n+1):
46+
for divisor in range(2, floor(sqrt(num)) + 1):
47+
# Definition of semiprime: divisor and quotient are prime
48+
if sieve[divisor] and num % divisor == 0 and sieve[num // divisor]:
49+
semiprime_count += 1
50+
prefix.append(semiprime_count)
51+
return [prefix[q[i]] - prefix[p[i]-1] for i in range(len(p))]
52+
53+
# Time Complexity:
54+
# Instantiating SPF (smallest prime factor) sieve over [0..N] = O(Nlog(logN))
55+
# Create prefix sum array = O(N)
56+
# Fill prefix sum array with O(1) work per element using SPF = O(N)
57+
# Do M queries, with O(1) work per element using prefix sum array = O(M)
58+
# Overall: O(Nlog(logN) + 2 * N + M) = O(Nlog(logN) + M)
59+
60+
def solution_b(n: int, p: list[int], q: list[int]) -> list[int]:
61+
"""Optimised solution generated by gemini-3-pro (explained in comments)"""
62+
63+
# spf[i] is the smallest prime factor for number i
64+
# If spf[i] == 0, then i is prime
65+
spf = [0] * (n+1)
66+
i = 2
67+
while i * i <= n:
68+
# Sieve if i is prime, recording the smallest prime factor
69+
if spf[i] == 0:
70+
k = i * i
71+
while k <= n:
72+
if spf[k] == 0:
73+
spf[k] = i
74+
k += i
75+
i += 1
76+
77+
# prefix[i] is the number of semiprimes up to and including number i
78+
prefix = [0] * (n + 1)
79+
for num in range(2, n + 1):
80+
# Composite number (candidate for semiprime)
81+
if spf[num] != 0:
82+
smallest_prime_factor = spf[num]
83+
complement = num // smallest_prime_factor
84+
if spf[complement] == 0:
85+
prefix[num] += 1
86+
prefix[num] += prefix[num - 1]
87+
88+
# Why dividing by SPF can verify semiprimes correctly:
89+
# 1. If N is prime (single prime factor), we don't enter the conditional (... if spf[num] != 0:)
90+
# 2. If N is semiprime (2 prime factors), dividing by SPF[N] returns the other prime factor
91+
# 3. If N has more than 2 prime factors (every other number), dividing by SPF[N]
92+
# results in another composite number (not prime)
93+
94+
return [prefix[q[i]] - prefix[p[i]-1] for i in range(len(p))]

0 commit comments

Comments
 (0)