Skip to content

Commit

Permalink
Add time complexity analyses
Browse files Browse the repository at this point in the history
  • Loading branch information
LivInTheLookingGlass committed Oct 21, 2024
1 parent 2f94d3e commit b0b6fb7
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 14 deletions.
4 changes: 4 additions & 0 deletions python/src/lib/factors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@


def proper_divisors(num: int) -> Iterator[int]:
"""Iterate over the proper divisors of a given number.
Given that it will generate a max of :math:`\\log(n)` factors, the binomial theorem tells us this should take
:math:`O(2^{\\log(n)}) = O(n)` operations."""
factors = tuple(prime_factors(num))
seen: Set[int] = set()
yield 1
Expand Down
8 changes: 6 additions & 2 deletions python/src/lib/fibonacci.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@


def fib() -> Iterator[int]:
"""This generator goes through the fibonacci sequence"""
"""This generator goes through the fibonacci sequence
Generating each new element happens in constant time."""
a, b = 0, 1
while True:
yield b
Expand All @@ -23,7 +25,9 @@ def fib_by_3(start_index: int = 0) -> Iterator[int]:
F[n] = 3 * F[n-3] + 2 * F[n-4]
F[n] = 3 * F[n-3] + F[n-4] + F[n-5] + F[n-6]
F[n] = 4 * F[n-3] + F[n-6]
"""
Generating each new element happens in constant time, and setup takes :math:`O(n)` where :math:`n` is the given
start index."""
orig = fib()
a = 0
consume(orig, start_index)
Expand Down
16 changes: 13 additions & 3 deletions python/src/lib/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@


def from_digits(digs: Iterable[int], base: int = 10) -> int:
"""Reconstruct a number from a series of digits."""
"""Reconstruct a number from a series of digits.
Runs in :math:`O(n)` operations, where :math:`n` is the number of digits. This means that it will take
:math:`O(\\log(m))` where :math:`m` is the original number."""
ret: int = 0
for dig in digs:
ret = ret * base + dig
Expand All @@ -16,7 +19,10 @@ def lattice_paths(height: int, width: int) -> int:


def mul_inv(a: int, b: int) -> int:
"""Multiplicative inverse for modulo numbers"""
"""Multiplicative inverse for modulo numbers
Runs in :math:`O(\\log(\\min(a, b)))` time. Given that this is typically used in the context of modulus math, we
can usually assume :math:`a < b`, simplifying this to :math:`O(\\log(a))` time."""
if b == 1:
return 1
b0: int = b
Expand All @@ -32,7 +38,11 @@ def mul_inv(a: int, b: int) -> int:


def n_choose_r(n: int, r: int) -> int:
"""Enumerate the number of ways to pick r elements from a collection of size n."""
"""Enumerate the number of ways to pick r elements from a collection of size n.
Runs in :math:`O(n)` multiplications. Because of how Python works, numbers less than :math:`2^{64}` will multiply
in constant time. Larger numbers, however, will multiply in :math:`O(n^{1.585})`, giving an overall time complexity
of :math:`O(n^{2.585})`."""
return factorial(n) // factorial(r) // factorial(n - r)


Expand Down
43 changes: 34 additions & 9 deletions python/src/lib/primes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@
def primes(stop: Optional[int] = None) -> Iterator[int]:
"""Iterates over the prime numbers up to an (optional) limit, with caching.
This iterator leverages the :py:func:`modified_eratosthenes` iterator, but adds
additional features. The biggest is a ``stop`` argument, which will prevent it
from yielding any numbers larger than that. The next is caching of any yielded
prime numbers."""
This iterator leverages the :py:func:`modified_eratosthenes` iterator, but adds additional features. The biggest
is a ``stop`` argument, which will prevent it from yielding any numbers larger than that. The next is caching of
any yielded prime numbers.
It takes :math:`O(n)` space as a global cache, where :math:`n` is the highest number yet generated, and
:math:`O(n \\cdot \\log(\\log(n)))` to generate primes up to a given limit of :math:`n, n < 2^{64}`. If :math:`n`
is *larger* than that, Python will shift to a different arithmetic model, which will shift us into
:math:`O(n \\cdot \\log(n)^{1.585} \\cdot \\log(\\log(n)))` time."""
if stop is None:
yield from cache
else:
Expand Down Expand Up @@ -81,7 +85,11 @@ def modified_eratosthenes() -> Iterator[int]:


def prime_factors(num: int) -> Iterator[int]:
"""Iterates over the prime factors of a number."""
"""Iterates over the prime factors of a number.
It takes :math:`O(m \\cdot \\log(m)^{1.585} \\cdot \\log(\\log(m)))` time to generate the needed primes, where
:math:`m = \\sqrt{n}`, so :math:`O(\\sqrt{n} \\cdot \\log(\\sqrt{n})^{1.585} \\cdot \\log(\\log(\\sqrt{n})))`,
which simplifies to :math:`O(\\sqrt{n} \\cdot \\log(n)^{1.585} \\cdot \\log(\\log(n)))` operations."""
if num < 0:
yield -1
num = -num
Expand Down Expand Up @@ -111,8 +119,12 @@ def is_prime(
) -> bool:
"""Detects if a number is prime or not.
If a count other than 1 is given, it returns True only if the number has
exactly count prime factors."""
If a count other than 1 is given, it returns True only if the number has exactly count prime factors.
This function has three cases. If count is :math:`1`, this function runs in
:math:`O(\\log(n)^{1.585} \\cdot \\log(\\log(n)))`. If count is :math:`c` and ``distinct=False``, it runs in
:math:`O(c \\cdot \\log(n)^{1.585} \\cdot \\log(\\log(n)))`. Otherwise, it runs in
:math:`O(\\sqrt{n} \\cdot \\log(n)^{1.585} \\cdot \\log(\\log(n)))` time."""
if num in (0, 1):
return False
factors = iter(prime_factors(num))
Expand Down Expand Up @@ -143,7 +155,10 @@ def primes_and_negatives(*args: int) -> Iterator[int]:


def fast_totient(n: int, factors: Optional[Iterable[int]] = None) -> int:
"""A shortcut method to calculate Euler's totient function which assumes n has *distinct* prime factors."""
"""A shortcut method to calculate Euler's totient function which assumes n has *distinct* prime factors.
It runs exactly 1 multiplication and 1 subtraction per prime factor, giving a worst case of
:math:`O(\\sqrt{n} \\cdot \\log(n)^{1.585 \\cdot 2} \\cdot \\log(\\log(n)))`."""
return reduce(lambda x, y: x * (y - 1), factors or prime_factors(n), 1)


Expand All @@ -152,7 +167,17 @@ def _reduce_factors(x: Fraction, y: int) -> Fraction:


def totient(n: int) -> int:
"""Calculates Euler's totient function in the general case."""
"""Calculates Euler's totient function in the general case.
Takes :math:`O(\\log(n))` fraction multiplications, each of which is dominated by two GCDs, which run at
:math:`O(\\log(\\min(a, b))^2)`. Given that the max factor is :math:`\\sqrt{n}`, we can simplify this to a worst
case of :math:`O(\\log(n) \\cdot \\log(\\sqrt{n}))^2 = O(\\log(n) \\cdot \\tfrac{1}{2} \\log(n)^2) = O(\\log(n)^3)`.
Computing the prime factors themselves takes :math:`O(\\sqrt{n} \\cdot \\log(n)^{1.585} \\cdot \\log(\\log(n)))`
when the cache is not initialized, but on all future runs will take :math:`O(\\log(n))` time.
This combines to give us :math:`O(\\sqrt{n} \\cdot \\log(n)^{4.585} \\cdot \\log(\\log(n)))` time when the cache is
stale, and :math:`O(\\log(n)^4)` time on future runs."""
total_factors = tuple(prime_factors(n))
unique_factors = set(total_factors)
if len(total_factors) == len(unique_factors):
Expand Down

0 comments on commit b0b6fb7

Please sign in to comment.