Skip to content

Commit 8aa1372

Browse files
committed
Improve prime factorization correctness and performance
1 parent ca5b8c1 commit 8aa1372

File tree

1 file changed

+118
-78
lines changed

1 file changed

+118
-78
lines changed

maths/gcd_of_n_numbers.py

Lines changed: 118 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,149 @@
11
"""
2-
Gcd of N Numbers
3-
Reference: https://en.wikipedia.org/wiki/Greatest_common_divisor
2+
GCD of N Numbers
3+
4+
This module calculates the Greatest Common Divisor (GCD)
5+
of multiple positive integers using prime factorization.
6+
7+
Reference:
8+
https://en.wikipedia.org/wiki/Greatest_common_divisor
49
"""
510

611
from collections import Counter
12+
from math import isqrt
713

814

915
def get_factors(
10-
number: int, factors: Counter | None = None, factor: int = 2
16+
number: int, prime_factors: Counter | None = None, divisor: int = 2
1117
) -> Counter:
1218
"""
13-
this is a recursive function for get all factors of number
19+
Recursively computes the PRIME FACTORIZATION of a positive integer.
20+
21+
The result is returned as a Counter where:
22+
- key → prime factor
23+
- value → power of that prime factor
24+
25+
Examples:
1426
>>> get_factors(45)
1527
Counter({3: 2, 5: 1})
1628
>>> get_factors(2520)
1729
Counter({2: 3, 3: 2, 5: 1, 7: 1})
1830
>>> get_factors(23)
1931
Counter({23: 1})
20-
>>> get_factors(0)
21-
Traceback (most recent call last):
22-
...
23-
TypeError: number must be integer and greater than zero
24-
>>> get_factors(-1)
25-
Traceback (most recent call last):
26-
...
27-
TypeError: number must be integer and greater than zero
28-
>>> get_factors(1.5)
29-
Traceback (most recent call last):
30-
...
31-
TypeError: number must be integer and greater than zero
32-
33-
factor can be all numbers from 2 to number that we check if number % factor == 0
34-
if it is equal to zero, we check again with number // factor
35-
else we increase factor by one
32+
>>> get_factors(1)
33+
Counter()
3634
"""
3735

38-
match number:
39-
case int(number) if number == 1:
40-
return Counter({1: 1})
41-
case int(num) if number > 0:
42-
number = num
43-
case _:
44-
raise TypeError("number must be integer and greater than zero")
45-
46-
factors = factors or Counter()
47-
48-
if number == factor: # break condition
49-
# all numbers are factors of itself
50-
factors[factor] += 1
51-
return factors
52-
53-
if number % factor > 0:
54-
# if it is greater than zero
55-
# so it is not a factor of number and we check next number
56-
return get_factors(number, factors, factor + 1)
57-
58-
factors[factor] += 1
59-
# else we update factors (that is Counter(dict-like) type) and check again
60-
return get_factors(number // factor, factors, factor)
36+
# -----------------------------
37+
# Input Validation
38+
# -----------------------------
39+
# The function should only accept positive integers.
40+
# Any other input is invalid.
41+
if not isinstance(number, int) or number <= 0:
42+
raise TypeError("number must be an integer greater than zero")
43+
44+
# -----------------------------
45+
# Base Case: number == 1
46+
# -----------------------------
47+
# 1 has NO prime factors.
48+
# Returning an empty Counter is mathematically correct.
49+
if number == 1:
50+
return Counter()
51+
52+
# -----------------------------
53+
# Initialize Counter
54+
# -----------------------------
55+
# If this is the first call, create a new Counter.
56+
# Otherwise, reuse the existing one (recursive calls).
57+
prime_factors = prime_factors or Counter()
58+
59+
# -----------------------------
60+
# Optimization (IMPORTANT)
61+
# -----------------------------
62+
# If divisor is greater than sqrt(number),
63+
# then the remaining number itself must be prime.
64+
#
65+
# Example:
66+
# If number = 23, no divisor <= sqrt(23) divides it,
67+
# so 23 is prime.
68+
if divisor > isqrt(number):
69+
prime_factors[number] += 1
70+
return prime_factors
71+
72+
# -----------------------------
73+
# If divisor divides the number
74+
# -----------------------------
75+
if number % divisor == 0:
76+
# divisor is a prime factor
77+
prime_factors[divisor] += 1
78+
79+
# Continue factoring the reduced number
80+
return get_factors(number // divisor, prime_factors, divisor)
81+
82+
# -----------------------------
83+
# If divisor does NOT divide the number
84+
# -----------------------------
85+
# Try the next possible divisor
86+
return get_factors(number, prime_factors, divisor + 1)
6187

6288

6389
def get_greatest_common_divisor(*numbers: int) -> int:
6490
"""
65-
get gcd of n numbers:
91+
Computes the Greatest Common Divisor (GCD)
92+
of one or more positive integers.
93+
94+
The approach:
95+
1. Compute prime factors of each number
96+
2. Find common factors using Counter intersection
97+
3. Multiply common factors to get GCD
98+
99+
Examples:
66100
>>> get_greatest_common_divisor(18, 45)
67101
9
68102
>>> get_greatest_common_divisor(23, 37)
69103
1
70104
>>> get_greatest_common_divisor(2520, 8350)
71105
10
72-
>>> get_greatest_common_divisor(-10, 20)
73-
Traceback (most recent call last):
74-
...
75-
Exception: numbers must be integer and greater than zero
76-
>>> get_greatest_common_divisor(1.5, 2)
77-
Traceback (most recent call last):
78-
...
79-
Exception: numbers must be integer and greater than zero
80-
>>> get_greatest_common_divisor(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
81-
1
82-
>>> get_greatest_common_divisor("1", 2, 3, 4, 5, 6, 7, 8, 9, 10)
83-
Traceback (most recent call last):
84-
...
85-
Exception: numbers must be integer and greater than zero
86106
"""
87107

88-
# we just need factors, not numbers itself
108+
# -----------------------------
109+
# Factorize all numbers
110+
# -----------------------------
111+
# map(get_factors, numbers) converts:
112+
# (18, 45) → [Counter({2:1,3:2}), Counter({3:2,5:1})]
89113
try:
90-
same_factors, *factors = map(get_factors, numbers)
91-
except TypeError as e:
92-
raise Exception("numbers must be integer and greater than zero") from e
93-
94-
for factor in factors:
95-
same_factors &= factor
96-
# get common factor between all
97-
# `&` return common elements with smaller value (for Counter type)
98-
99-
# now, same_factors is something like {2: 2, 3: 4} that means 2 * 2 * 3 * 3 * 3 * 3
100-
mult = 1
101-
# power each factor and multiply
102-
# for {2: 2, 3: 4}, it is [4, 81] and then 324
103-
for m in [factor**power for factor, power in same_factors.items()]:
104-
mult *= m
105-
return mult
106-
107-
114+
common_factors, *other_factors = map(get_factors, numbers)
115+
except TypeError as exc:
116+
# Raised if any input is invalid
117+
raise ValueError("numbers must be integers greater than zero") from exc
118+
119+
# -----------------------------
120+
# Find common prime factors
121+
# -----------------------------
122+
# Counter '&' operator keeps:
123+
# - only common keys
124+
# - minimum power for each key
125+
#
126+
# Example:
127+
# Counter({2:1, 3:2}) & Counter({3:2, 5:1})
128+
# → Counter({3:2})
129+
for factors in other_factors:
130+
common_factors &= factors
131+
132+
# -----------------------------
133+
# Multiply common factors
134+
# -----------------------------
135+
# Convert factorization back into a number.
136+
# Example:
137+
# Counter({2:2, 3:1}) → 2² * 3¹ = 12
138+
gcd_value = 1
139+
for factor, power in common_factors.items():
140+
gcd_value *= factor ** power
141+
142+
return gcd_value
143+
144+
145+
# -----------------------------
146+
# Manual Test
147+
# -----------------------------
108148
if __name__ == "__main__":
109-
print(get_greatest_common_divisor(18, 45)) # 9
149+
print(get_greatest_common_divisor(18, 45)) # Expected output: 9

0 commit comments

Comments
 (0)