diff --git a/python/src/lib/factors.py b/python/src/lib/factors.py index 1e1d085d..9ea35bc8 100644 --- a/python/src/lib/factors.py +++ b/python/src/lib/factors.py @@ -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 diff --git a/python/src/lib/fibonacci.py b/python/src/lib/fibonacci.py index f55df3e8..c2bffaa8 100644 --- a/python/src/lib/fibonacci.py +++ b/python/src/lib/fibonacci.py @@ -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 @@ -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) diff --git a/python/src/lib/math.py b/python/src/lib/math.py index 84e876c5..f7dd30b0 100644 --- a/python/src/lib/math.py +++ b/python/src/lib/math.py @@ -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 @@ -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 @@ -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) diff --git a/python/src/lib/primes.py b/python/src/lib/primes.py index 8a98ac9b..4c55ed65 100644 --- a/python/src/lib/primes.py +++ b/python/src/lib/primes.py @@ -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: @@ -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 @@ -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)) @@ -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) @@ -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):