Module grscheller.boring_math.integer_math

Boring Math integer library

Library of functions of an integer pure math nature.

Expand source code
# Copyright 2016-2023 Geoffrey R. Scheller
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Boring Math integer library

Library of functions of an integer pure math nature.
"""

from __future__ import annotations

import sys
from typing import Iterator, Tuple
from grscheller.circular_array import CircularArray

__all__ = ['gcd', 'lcm', 'mkCoprime', 'primes', 'comb',
           'pythag3', 'ackermann', 'fibonacci']

# Number Theory mathematical Functions.

def gcd(fst: int, snd: int) -> int:
    """Uses Euclidean algorithm to compute the gcd of two integers

    Takes two integers, returns gcd >= 0.

    Note: gcd(0,0) returns 0 but in this case the gcd does not exist
    """
    fst, snd = abs(fst), abs(snd)
    fst, snd = (fst, snd) if fst > snd else (snd, fst)
    while snd > 0:
        fst, snd = snd, fst % snd

    return fst

def lcm(fst: int, snd: int) -> int:
    """Finds the least common multiple of two integers.
       Takes two integers, returns lcm >= 0.
    """
    common = 1 if 0 == fst == snd else gcd(fst, snd)
    fst //= common

    return abs(fst*snd)

def mkCoprime(fst: int, snd: int) -> Tuple(int, int):
    '''Makes 2 integers coprime by dividing out their common factors.

       Note: One use case is when dividing two factored BigInts. This is the main
             motivation for choosing mkCoprime(0, 0) = (0, 0) instead of (1, 1).
    '''
    common = 1 if 0 == fst == snd else gcd(fst, snd)
    return fst // common, snd // common

def primes(start: int=2, end_before: int=100) -> Iterator:
    """Return an iterator for the prime numbers Using the Sieve of Eratosthenes
    algorithm.
    """
    if start >= end_before or end_before < 3:
        return []
    if start < 2:
        start = 2

    sieve = [x for x in range(3, end_before, 2) if x % 3 != 0]
    stop = int(end_before**(0.5)) + 1
    front = -1
    for prime in sieve:
        front += 1
        if prime > stop:
            break
        for pot_prime in sieve[-1:front:-1]:
            if pot_prime % prime == 0:
                sieve.remove(pot_prime)

    if start <= 3 < end_before:  # We missed [2, 3] but
        sieve.insert(0, 3)       # saved about 60% for
    if start <= 2 < end_before:  # the initial storage
        sieve.insert(0, 2)       # space.

    # return sieve after trimming unwanted values
    return (x for x in sieve if x >= start)

# Combinatorics

def comb(n: int, m: int, factorsNumerator: int=66, factorsDenominator: int=4) -> int:
    """Implements C(n,m), the number of combinations of n items taken m at a time,
    in a way that works efficiently for Python's arbitrary length integers. Compares
    well with math.comb() which is probably C code.

    Raises: ValueError if n < 0 or m < 0
    """
    if n < 0 or m < 0:
        raise ValueError('for C(n, m) n and m must be a non-negavive ints')
    if n == m or m == 0:
        return 1
    elif m > n:
        return 0

    # using C(n, m) = C(n, n-m) to reduce number of factors in calculation
    if m > (n // 2):
        m = n - m

    def compact(ca: CircularArray, targetSize: int) -> CircularArray:
        """Reduce the length of the circular array by factors of 2 by
        combinding factors from each end, O(ln(n)).
        """
        ca1 = ca
        while len(ca1) > targetSize:
            ca2 = CircularArray()
            size = len(ca1)
            if size % 2 == 1:
                ca1.pushR(ca1.popL() * ca1.popR())
                size -= 1
            for ii in range(size // 2):
                ca2.pushL(ca1[ii] * ca1[size - ii - 1])
            ca1 = ca2
        return ca1

    # Prepare data structures
    topFactors = CircularArray(*range(n - m + 1, n + 1))
    bottomFactors = CircularArray(*range(m, 1, -1))

    # Compact data structures
    topFactors = compact(topFactors, factorsNumerator)
    bottomFactors = compact(bottomFactors, factorsDenominator)

    # Cancel all factors in denominator before multiplying
    # the remaining factors in the numerator.
    for bottom in bottomFactors:
        for ii in range(len(topFactors)):
            top, bottom = mkCoprime(topFactors.popL(), bottom)
            if top > 1:
                topFactors.pushR(top)
            if bottom == 1:
                break

    return topFactors.foldL(lambda x, y: x * y)

# Pythagorean Triples

def pythag3(a_max: int=3, all_max: int|None=None) -> Iterator:
    """This iterator finds all primative pythagorean triples
    up to a given level.  A Pythagorean triple are three
    integers (a,b,c) such that a^2 + b^2 = c^2 where
    x,y,z > 0 and gcd(a,b,c) = 1

    If called with one argument, generates all triples with
    a <= a_max

    If called with two arguments generate all triples with
    a <= a_max and a,b,c <= all_max
    """
    def cap_max_abc(a_max: int, all_max: int=None) -> int:
        """Returns capped max values for sides a,b,c where
        based on a_max and all_max given by caller of pythag3.

        note: a_max and c_max are integers
        note: b_max is a function of side a
        """
        # Limit values to those where geometry
        # based optimization assumptions hold.
        if a_max < 3:
            a_max = 2

        # For a given value of a, theoretically there
        # are no more triples beyond this value of b.
        def b_max_uncapped(a):
            return (a**2 - 1)//2

        if all_max is None:
            b_max = b_max_uncapped
        else:
            if all_max < 5:
                all_max = 4
            if all_max < a_max + 2:
                a_max = all_max - 2

            def b_max_capped(a):
                return min((b_max_uncapped(a), int((all_max**2 - a**2)**0.5)))

            b_max = b_max_capped

        c_max = int((a_max**2 + b_max(a_max)**2)**(0.5)) + 1

        return a_max, b_max, c_max

    # Cap triples to those with sides no bigger than all_max
    a_max, b_max, c_max = cap_max_abc(a_max, all_max)

    # Hypothrnuse perfect square lookup dictionary
    # Note: hypotenuse always odd for Pythagorean triples
    squares = {h*h: h for h in range(5, c_max + 1, 2)}

    # Calculate Pythagorean triples
    for side_a in range(3, a_max + 1):
        for side_b in range(side_a + 1, b_max(side_a) + 1):
            csq = side_a**2 + side_b**2
            if csq in squares:
                if gcd(side_a, side_b) == 1:
                    yield side_a, side_b, squares[csq]

# Computable but not primitive recursive functions

def ackermann(m: int, n:int) -> int:
    """Ackermann function is defined recursively by:

    ackermann(0,n) = n+1
    ackermann(m,0) = ackermann(m-1,1)
    ackermann(m,n) = ackermann(m-1, ackermann(m, n-1)) for n,m > 0

    Ackerman's function is an example of a function that is computable
    but not primatively recursive. It quickly becomes computationally
    intractable for relatively small values of m and n.
    """
    # Model a function stack with a list, then
    # evaluate innermost ackermann function first.
    acker = [m, n]

    while len(acker) > 1:
        mm, nn = acker[-2:]
        if mm < 1:
            acker[-1] = acker.pop() + 1
        elif nn < 1:
            acker[-2] = acker[-2] - 1
            acker[-1] = 1
        else:
            acker[-2] = mm - 1
            acker[-1] = mm
            acker.append(nn-1)

    return acker[0]

# Fibonacci Iterator

def fibonacci(fib0: int, fib1: int) -> Iterator:
    """Returns an iterator to a Fibonacci sequence whose
    first two terms are fib0 and fib1.
    """
    while True:
        yield fib0
        fib0, fib1 = fib1, fib0+fib1

if __name__ == '__main__':
    sys.exit(0)

Functions

def ackermann(m: int, n: int) ‑> int

Ackermann function is defined recursively by:

ackermann(0,n) = n+1 ackermann(m,0) = ackermann(m-1,1) ackermann(m,n) = ackermann(m-1, ackermann(m, n-1)) for n,m > 0

Ackerman's function is an example of a function that is computable but not primatively recursive. It quickly becomes computationally intractable for relatively small values of m and n.

Expand source code
def ackermann(m: int, n:int) -> int:
    """Ackermann function is defined recursively by:

    ackermann(0,n) = n+1
    ackermann(m,0) = ackermann(m-1,1)
    ackermann(m,n) = ackermann(m-1, ackermann(m, n-1)) for n,m > 0

    Ackerman's function is an example of a function that is computable
    but not primatively recursive. It quickly becomes computationally
    intractable for relatively small values of m and n.
    """
    # Model a function stack with a list, then
    # evaluate innermost ackermann function first.
    acker = [m, n]

    while len(acker) > 1:
        mm, nn = acker[-2:]
        if mm < 1:
            acker[-1] = acker.pop() + 1
        elif nn < 1:
            acker[-2] = acker[-2] - 1
            acker[-1] = 1
        else:
            acker[-2] = mm - 1
            acker[-1] = mm
            acker.append(nn-1)

    return acker[0]
def comb(n: int, m: int, factorsNumerator: int = 66, factorsDenominator: int = 4) ‑> int

Implements C(n,m), the number of combinations of n items taken m at a time, in a way that works efficiently for Python's arbitrary length integers. Compares well with math.comb() which is probably C code.

Raises: ValueError if n < 0 or m < 0

Expand source code
def comb(n: int, m: int, factorsNumerator: int=66, factorsDenominator: int=4) -> int:
    """Implements C(n,m), the number of combinations of n items taken m at a time,
    in a way that works efficiently for Python's arbitrary length integers. Compares
    well with math.comb() which is probably C code.

    Raises: ValueError if n < 0 or m < 0
    """
    if n < 0 or m < 0:
        raise ValueError('for C(n, m) n and m must be a non-negavive ints')
    if n == m or m == 0:
        return 1
    elif m > n:
        return 0

    # using C(n, m) = C(n, n-m) to reduce number of factors in calculation
    if m > (n // 2):
        m = n - m

    def compact(ca: CircularArray, targetSize: int) -> CircularArray:
        """Reduce the length of the circular array by factors of 2 by
        combinding factors from each end, O(ln(n)).
        """
        ca1 = ca
        while len(ca1) > targetSize:
            ca2 = CircularArray()
            size = len(ca1)
            if size % 2 == 1:
                ca1.pushR(ca1.popL() * ca1.popR())
                size -= 1
            for ii in range(size // 2):
                ca2.pushL(ca1[ii] * ca1[size - ii - 1])
            ca1 = ca2
        return ca1

    # Prepare data structures
    topFactors = CircularArray(*range(n - m + 1, n + 1))
    bottomFactors = CircularArray(*range(m, 1, -1))

    # Compact data structures
    topFactors = compact(topFactors, factorsNumerator)
    bottomFactors = compact(bottomFactors, factorsDenominator)

    # Cancel all factors in denominator before multiplying
    # the remaining factors in the numerator.
    for bottom in bottomFactors:
        for ii in range(len(topFactors)):
            top, bottom = mkCoprime(topFactors.popL(), bottom)
            if top > 1:
                topFactors.pushR(top)
            if bottom == 1:
                break

    return topFactors.foldL(lambda x, y: x * y)
def fibonacci(fib0: int, fib1: int) ‑> Iterator

Returns an iterator to a Fibonacci sequence whose first two terms are fib0 and fib1.

Expand source code
def fibonacci(fib0: int, fib1: int) -> Iterator:
    """Returns an iterator to a Fibonacci sequence whose
    first two terms are fib0 and fib1.
    """
    while True:
        yield fib0
        fib0, fib1 = fib1, fib0+fib1
def gcd(fst: int, snd: int) ‑> int

Uses Euclidean algorithm to compute the gcd of two integers

Takes two integers, returns gcd >= 0.

Note: gcd(0,0) returns 0 but in this case the gcd does not exist

Expand source code
def gcd(fst: int, snd: int) -> int:
    """Uses Euclidean algorithm to compute the gcd of two integers

    Takes two integers, returns gcd >= 0.

    Note: gcd(0,0) returns 0 but in this case the gcd does not exist
    """
    fst, snd = abs(fst), abs(snd)
    fst, snd = (fst, snd) if fst > snd else (snd, fst)
    while snd > 0:
        fst, snd = snd, fst % snd

    return fst
def lcm(fst: int, snd: int) ‑> int

Finds the least common multiple of two integers. Takes two integers, returns lcm >= 0.

Expand source code
def lcm(fst: int, snd: int) -> int:
    """Finds the least common multiple of two integers.
       Takes two integers, returns lcm >= 0.
    """
    common = 1 if 0 == fst == snd else gcd(fst, snd)
    fst //= common

    return abs(fst*snd)
def mkCoprime(fst: int, snd: int) ‑> Tuple(int, int)

Makes 2 integers coprime by dividing out their common factors.

Note: One use case is when dividing two factored BigInts. This is the main motivation for choosing mkCoprime(0, 0) = (0, 0) instead of (1, 1).

Expand source code
def mkCoprime(fst: int, snd: int) -> Tuple(int, int):
    '''Makes 2 integers coprime by dividing out their common factors.

       Note: One use case is when dividing two factored BigInts. This is the main
             motivation for choosing mkCoprime(0, 0) = (0, 0) instead of (1, 1).
    '''
    common = 1 if 0 == fst == snd else gcd(fst, snd)
    return fst // common, snd // common
def primes(start: int = 2, end_before: int = 100) ‑> Iterator

Return an iterator for the prime numbers Using the Sieve of Eratosthenes algorithm.

Expand source code
def primes(start: int=2, end_before: int=100) -> Iterator:
    """Return an iterator for the prime numbers Using the Sieve of Eratosthenes
    algorithm.
    """
    if start >= end_before or end_before < 3:
        return []
    if start < 2:
        start = 2

    sieve = [x for x in range(3, end_before, 2) if x % 3 != 0]
    stop = int(end_before**(0.5)) + 1
    front = -1
    for prime in sieve:
        front += 1
        if prime > stop:
            break
        for pot_prime in sieve[-1:front:-1]:
            if pot_prime % prime == 0:
                sieve.remove(pot_prime)

    if start <= 3 < end_before:  # We missed [2, 3] but
        sieve.insert(0, 3)       # saved about 60% for
    if start <= 2 < end_before:  # the initial storage
        sieve.insert(0, 2)       # space.

    # return sieve after trimming unwanted values
    return (x for x in sieve if x >= start)
def pythag3(a_max: int = 3, all_max: int | None = None) ‑> Iterator

This iterator finds all primative pythagorean triples up to a given level. A Pythagorean triple are three integers (a,b,c) such that a^2 + b^2 = c^2 where x,y,z > 0 and gcd(a,b,c) = 1

If called with one argument, generates all triples with a <= a_max

If called with two arguments generate all triples with a <= a_max and a,b,c <= all_max

Expand source code
def pythag3(a_max: int=3, all_max: int|None=None) -> Iterator:
    """This iterator finds all primative pythagorean triples
    up to a given level.  A Pythagorean triple are three
    integers (a,b,c) such that a^2 + b^2 = c^2 where
    x,y,z > 0 and gcd(a,b,c) = 1

    If called with one argument, generates all triples with
    a <= a_max

    If called with two arguments generate all triples with
    a <= a_max and a,b,c <= all_max
    """
    def cap_max_abc(a_max: int, all_max: int=None) -> int:
        """Returns capped max values for sides a,b,c where
        based on a_max and all_max given by caller of pythag3.

        note: a_max and c_max are integers
        note: b_max is a function of side a
        """
        # Limit values to those where geometry
        # based optimization assumptions hold.
        if a_max < 3:
            a_max = 2

        # For a given value of a, theoretically there
        # are no more triples beyond this value of b.
        def b_max_uncapped(a):
            return (a**2 - 1)//2

        if all_max is None:
            b_max = b_max_uncapped
        else:
            if all_max < 5:
                all_max = 4
            if all_max < a_max + 2:
                a_max = all_max - 2

            def b_max_capped(a):
                return min((b_max_uncapped(a), int((all_max**2 - a**2)**0.5)))

            b_max = b_max_capped

        c_max = int((a_max**2 + b_max(a_max)**2)**(0.5)) + 1

        return a_max, b_max, c_max

    # Cap triples to those with sides no bigger than all_max
    a_max, b_max, c_max = cap_max_abc(a_max, all_max)

    # Hypothrnuse perfect square lookup dictionary
    # Note: hypotenuse always odd for Pythagorean triples
    squares = {h*h: h for h in range(5, c_max + 1, 2)}

    # Calculate Pythagorean triples
    for side_a in range(3, a_max + 1):
        for side_b in range(side_a + 1, b_max(side_a) + 1):
            csq = side_a**2 + side_b**2
            if csq in squares:
                if gcd(side_a, side_b) == 1:
                    yield side_a, side_b, squares[csq]