Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 118 additions & 78 deletions maths/gcd_of_n_numbers.py
Original file line number Diff line number Diff line change
@@ -1,109 +1,149 @@
"""
Gcd of N Numbers
Reference: https://en.wikipedia.org/wiki/Greatest_common_divisor
GCD of N Numbers

This module calculates the Greatest Common Divisor (GCD)
of multiple positive integers using prime factorization.

Reference:
https://en.wikipedia.org/wiki/Greatest_common_divisor
"""

from collections import Counter
from math import isqrt


def get_factors(
number: int, factors: Counter | None = None, factor: int = 2
number: int, prime_factors: Counter | None = None, divisor: int = 2
) -> Counter:
"""
this is a recursive function for get all factors of number
Recursively computes the PRIME FACTORIZATION of a positive integer.

The result is returned as a Counter where:
- key → prime factor
- value → power of that prime factor

Examples:
>>> get_factors(45)
Counter({3: 2, 5: 1})
>>> get_factors(2520)
Counter({2: 3, 3: 2, 5: 1, 7: 1})
>>> get_factors(23)
Counter({23: 1})
>>> get_factors(0)
Traceback (most recent call last):
...
TypeError: number must be integer and greater than zero
>>> get_factors(-1)
Traceback (most recent call last):
...
TypeError: number must be integer and greater than zero
>>> get_factors(1.5)
Traceback (most recent call last):
...
TypeError: number must be integer and greater than zero

factor can be all numbers from 2 to number that we check if number % factor == 0
if it is equal to zero, we check again with number // factor
else we increase factor by one
>>> get_factors(1)
Counter()
"""

match number:
case int(number) if number == 1:
return Counter({1: 1})
case int(num) if number > 0:
number = num
case _:
raise TypeError("number must be integer and greater than zero")

factors = factors or Counter()

if number == factor: # break condition
# all numbers are factors of itself
factors[factor] += 1
return factors

if number % factor > 0:
# if it is greater than zero
# so it is not a factor of number and we check next number
return get_factors(number, factors, factor + 1)

factors[factor] += 1
# else we update factors (that is Counter(dict-like) type) and check again
return get_factors(number // factor, factors, factor)
# -----------------------------
# Input Validation
# -----------------------------
# The function should only accept positive integers.
# Any other input is invalid.
if not isinstance(number, int) or number <= 0:
raise TypeError("number must be an integer greater than zero")

# -----------------------------
# Base Case: number == 1
# -----------------------------
# 1 has NO prime factors.
# Returning an empty Counter is mathematically correct.
if number == 1:
return Counter()

# -----------------------------
# Initialize Counter
# -----------------------------
# If this is the first call, create a new Counter.
# Otherwise, reuse the existing one (recursive calls).
prime_factors = prime_factors or Counter()

# -----------------------------
# Optimization (IMPORTANT)
# -----------------------------
# If divisor is greater than sqrt(number),
# then the remaining number itself must be prime.
#
# Example:
# If number = 23, no divisor <= sqrt(23) divides it,
# so 23 is prime.
if divisor > isqrt(number):
prime_factors[number] += 1
return prime_factors

# -----------------------------
# If divisor divides the number
# -----------------------------
if number % divisor == 0:
# divisor is a prime factor
prime_factors[divisor] += 1

# Continue factoring the reduced number
return get_factors(number // divisor, prime_factors, divisor)

# -----------------------------
# If divisor does NOT divide the number
# -----------------------------
# Try the next possible divisor
return get_factors(number, prime_factors, divisor + 1)


def get_greatest_common_divisor(*numbers: int) -> int:
"""
get gcd of n numbers:
Computes the Greatest Common Divisor (GCD)
of one or more positive integers.

The approach:
1. Compute prime factors of each number
2. Find common factors using Counter intersection
3. Multiply common factors to get GCD

Examples:
>>> get_greatest_common_divisor(18, 45)
9
>>> get_greatest_common_divisor(23, 37)
1
>>> get_greatest_common_divisor(2520, 8350)
10
>>> get_greatest_common_divisor(-10, 20)
Traceback (most recent call last):
...
Exception: numbers must be integer and greater than zero
>>> get_greatest_common_divisor(1.5, 2)
Traceback (most recent call last):
...
Exception: numbers must be integer and greater than zero
>>> get_greatest_common_divisor(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
1
>>> get_greatest_common_divisor("1", 2, 3, 4, 5, 6, 7, 8, 9, 10)
Traceback (most recent call last):
...
Exception: numbers must be integer and greater than zero
"""

# we just need factors, not numbers itself
# -----------------------------
# Factorize all numbers
# -----------------------------
# map(get_factors, numbers) converts:
# (18, 45) → [Counter({2:1,3:2}), Counter({3:2,5:1})]
try:
same_factors, *factors = map(get_factors, numbers)
except TypeError as e:
raise Exception("numbers must be integer and greater than zero") from e

for factor in factors:
same_factors &= factor
# get common factor between all
# `&` return common elements with smaller value (for Counter type)

# now, same_factors is something like {2: 2, 3: 4} that means 2 * 2 * 3 * 3 * 3 * 3
mult = 1
# power each factor and multiply
# for {2: 2, 3: 4}, it is [4, 81] and then 324
for m in [factor**power for factor, power in same_factors.items()]:
mult *= m
return mult


common_factors, *other_factors = map(get_factors, numbers)
except TypeError as exc:
# Raised if any input is invalid
raise ValueError("numbers must be integers greater than zero") from exc

# -----------------------------
# Find common prime factors
# -----------------------------
# Counter '&' operator keeps:
# - only common keys
# - minimum power for each key
#
# Example:
# Counter({2:1, 3:2}) & Counter({3:2, 5:1})
# → Counter({3:2})
for factors in other_factors:
common_factors &= factors

# -----------------------------
# Multiply common factors
# -----------------------------
# Convert factorization back into a number.
# Example:
# Counter({2:2, 3:1}) → 2² * 3¹ = 12
gcd_value = 1
for factor, power in common_factors.items():
gcd_value *= factor**power

return gcd_value


# -----------------------------
# Manual Test
# -----------------------------
if __name__ == "__main__":
print(get_greatest_common_divisor(18, 45)) # 9
print(get_greatest_common_divisor(18, 45)) # Expected output: 9