diff --git a/README.md b/README.md index f89199b..cc7346d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ [![Tests](https://github.com/pralav-25/python-algorithm-lab/actions/workflows/ci.yml/badge.svg)](https://github.com/pralav-25/python-algorithm-lab/actions/workflows/ci.yml) +[![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue)](pyproject.toml) +[![License: MIT](https://img.shields.io/badge/License-MIT-green)](LICENSE) + Readable implementations of core algorithms, with executable examples and independent regression tests. Python 3.11+; no runtime dependencies. @@ -30,7 +33,7 @@ python -m doctest algorithm_lab/*.py Optional installation: `python -m pip install -e .`. For linting and formatting, install `.[dev]` and run `ruff check .` and `ruff format --check .`. -## Catalog (100 algorithms) +## Catalog (127 algorithms) | Module | Topic | What it demonstrates | | --- | --- | --- | @@ -134,6 +137,33 @@ install `.[dev]` and run `ruff check .` and `ruff format --check .`. | [n_queens](algorithm_lab/n_queens.py) | Backtracking | Lazy nonattacking queen placements using bit masks | | [fractional_knapsack](algorithm_lab/fractional_knapsack.py) | Optimization | Exact rational fractional-knapsack value and item fractions | | [weighted_interval_scheduling](algorithm_lab/weighted_interval_scheduling.py) | Optimization | Maximum-value compatible intervals with original-index reconstruction | +| [modular_inverse](algorithm_lab/modular_inverse.py) | Number theory | Extended-Euclidean modular inverses | +| [euler_totient](algorithm_lab/euler_totient.py) | Number theory | Count coprime residues by prime factorization | +| [mobius_sieve](algorithm_lab/mobius_sieve.py) | Number theory | Linear sieve of the Mobius function | +| [divisor_sieve](algorithm_lab/divisor_sieve.py) | Number theory | Compute every positive divisor list through a bound | +| [segmented_sieve](algorithm_lab/segmented_sieve.py) | Number theory | Find primes in a half-open interval with segmented marking | +| [integer_nth_root](algorithm_lab/integer_nth_root.py) | Number theory | Exact floor roots using integer binary search | +| [continued_fraction](algorithm_lab/continued_fraction.py) | Number theory | Canonical finite continued fractions for rational inputs | +| [continued_fraction_convergents](algorithm_lab/continued_fraction_convergents.py) | Number theory | Build exact rational convergents with a two-term recurrence | +| [stern_brocot_path](algorithm_lab/stern_brocot_path.py) | Number theory | Run-length paths to positive rationals in the Stern-Brocot tree | +| [multiplicative_order](algorithm_lab/multiplicative_order.py) | Number theory | Find the order of an invertible residue | +| [polynomial_evaluate](algorithm_lab/polynomial_evaluate.py) | Algebra | Horner evaluation of integer polynomials | +| [polynomial_multiply](algorithm_lab/polynomial_multiply.py) | Algebra | Exact dense polynomial convolution | +| [polynomial_divmod](algorithm_lab/polynomial_divmod.py) | Algebra | Exact polynomial long division over rational coefficients | +| [lagrange_interpolation](algorithm_lab/lagrange_interpolation.py) | Algebra | Evaluate the interpolating polynomial with exact rationals | +| [gaussian_elimination](algorithm_lab/gaussian_elimination.py) | Algebra | Solve square rational linear systems by pivoted elimination | +| [determinant](algorithm_lab/determinant.py) | Algebra | Compute exact determinants by elimination | +| [matrix_power](algorithm_lab/matrix_power.py) | Algebra | Integer matrix powers by repeated squaring | +| [linear_recurrence](algorithm_lab/linear_recurrence.py) | Algebra | Evaluate integer recurrences with a companion matrix | +| [stirling_second](algorithm_lab/stirling_second.py) | Combinatorics | Count partitions into a specified number of nonempty blocks | +| [combination_rank](algorithm_lab/combination_rank.py) | Combinatorics | Lexicographic ranking of fixed-size combinations | +| [combination_unrank](algorithm_lab/combination_unrank.py) | Combinatorics | Select a lexicographic combination without enumerating predecessors | +| [permutation_rank](algorithm_lab/permutation_rank.py) | Combinatorics | Lehmer-code ranking of permutations of range(n) | +| [permutation_unrank](algorithm_lab/permutation_unrank.py) | Combinatorics | Factoradic selection of a lexicographic permutation | +| [multiset_permutations](algorithm_lab/multiset_permutations.py) | Combinatorics | Generate distinct integer permutations in lexicographic order | +| [bounded_compositions](algorithm_lab/bounded_compositions.py) | Combinatorics | Enumerate fixed-sum tuples under per-position upper bounds | +| [subset_zeta_transform](algorithm_lab/subset_zeta_transform.py) | Combinatorics | Sum values over every subset of each bit mask | +| [subset_mobius_transform](algorithm_lab/subset_mobius_transform.py) | Combinatorics | Invert subset sums by Mobius inversion on bit masks | ## Validation diff --git a/algorithm_lab/bounded_compositions.py b/algorithm_lab/bounded_compositions.py new file mode 100644 index 0000000..74cedda --- /dev/null +++ b/algorithm_lab/bounded_compositions.py @@ -0,0 +1,44 @@ +"""Enumerate fixed-sum tuples under per-position upper bounds. + +total and each bound are nonnegative integers excluding bool. Yield tuples +in lexicographic order, with 0 <= value[i] <= bounds[i] and the given sum. +No bounds yields () only for total=0. An explicit stack avoids recursion; +O(k) working space, and O(k) work per yielded tuple plus visited prefixes. + +>>> list(bounded_compositions(2, [1, 2])) +[(0, 2), (1, 1)] +""" + +from algorithm_lab._validation import integer + + +def bounded_compositions(total, bounds): + integer(total, "total", minimum=0) + bounds = list(bounds) + for bound in bounds: + integer(bound, "bound", minimum=0) + suffix = [0] * (len(bounds) + 1) + for i in range(len(bounds) - 1, -1, -1): + suffix[i] = suffix[i + 1] + bounds[i] + if not bounds: + if total == 0: + yield () + return + + def choices(i, remaining): + return iter(range(max(0, remaining - suffix[i + 1]), min(bounds[i], remaining) + 1)) + + prefix = [] + stack = [(total, choices(0, total))] + while stack: + remaining, iterator = stack[-1] + value = next(iterator, None) + if value is None: + stack.pop() + if prefix: + prefix.pop() + elif len(stack) == len(bounds): + yield tuple(prefix + [value]) + else: + prefix.append(value) + stack.append((remaining - value, choices(len(prefix), remaining - value))) diff --git a/algorithm_lab/combination_rank.py b/algorithm_lab/combination_rank.py new file mode 100644 index 0000000..c20f9ef --- /dev/null +++ b/algorithm_lab/combination_rank.py @@ -0,0 +1,28 @@ +"""Lexicographic ranking of fixed-size combinations. + +n is a nonnegative integer, and combination is a strictly increasing sequence +of integers from range(n), excluding bool. Return its zero-based index among +itertools.combinations(range(n), k). Empty combinations rank 0. +O(n) binomial evaluations and O(k) copied storage. + +>>> combination_rank(5, [1, 3]) +5 +""" + +from math import comb + +from algorithm_lab._validation import integer + + +def combination_rank(n, combination): + integer(n, "n", minimum=0) + values = list(combination) + previous, rank, k = -1, 0, len(values) + for i, value in enumerate(values): + integer(value, "element", minimum=0) + if value <= previous or value >= n: + raise ValueError("combination must be strictly increasing within range(n)") + for skipped in range(previous + 1, value): + rank += comb(n - skipped - 1, k - i - 1) + previous = value + return rank diff --git a/algorithm_lab/combination_unrank.py b/algorithm_lab/combination_unrank.py new file mode 100644 index 0000000..bbf291b --- /dev/null +++ b/algorithm_lab/combination_unrank.py @@ -0,0 +1,30 @@ +"""Select a lexicographic combination without enumerating predecessors. + +Integers satisfy 0 <= k <= n and 0 <= rank < C(n,k); bool is rejected. +Return a list matching itertools.combinations(range(n), k)[rank]. O(n) +binomial evaluations and O(k) output space. The empty combination has rank 0. + +>>> combination_unrank(5, 2, 5) +[1, 3] +""" + +from math import comb + +from algorithm_lab._validation import integer + + +def combination_unrank(n, k, rank): + integer(n, "n", minimum=0) + integer(k, "k", minimum=0) + integer(rank, "rank", minimum=0) + if k > n or rank >= comb(n, k): + raise ValueError("combination rank is out of range") + result, candidate = [], 0 + while len(result) < k: + count = comb(n - candidate - 1, k - len(result) - 1) + if rank < count: + result.append(candidate) + else: + rank -= count + candidate += 1 + return result diff --git a/algorithm_lab/continued_fraction.py b/algorithm_lab/continued_fraction.py new file mode 100644 index 0000000..93d7506 --- /dev/null +++ b/algorithm_lab/continued_fraction.py @@ -0,0 +1,27 @@ +"""Canonical finite continued fractions for rational inputs. + +Accept integer numerator and nonzero integer denominator, excluding bool. +Return [a0, a1, ...], with a0 the floor and later terms positive. The final +term is > 1 unless the result is integral. O(log(max(|n|, |d|)+1)) Euclidean +steps and O(number of terms) output space. + +>>> continued_fraction(43, 19) +[2, 3, 1, 4] +""" + +from algorithm_lab._validation import integer + + +def continued_fraction(numerator, denominator): + integer(numerator, "numerator") + integer(denominator, "denominator") + if denominator == 0: + raise ValueError("denominator must be nonzero") + if denominator < 0: + numerator, denominator = -numerator, -denominator + result = [] + while denominator: + term, remainder = divmod(numerator, denominator) + result.append(term) + numerator, denominator = denominator, remainder + return result diff --git a/algorithm_lab/continued_fraction_convergents.py b/algorithm_lab/continued_fraction_convergents.py new file mode 100644 index 0000000..94b6b27 --- /dev/null +++ b/algorithm_lab/continued_fraction_convergents.py @@ -0,0 +1,24 @@ +"""Build exact rational convergents with a two-term recurrence. + +terms is an iterable of integers excluding bool; only the first may be +nonpositive. Return Fractions for all prefixes, or [] for empty input. +O(n) arithmetic operations and O(n) output space. + +>>> continued_fraction_convergents([1, 2, 2]) +[Fraction(1, 1), Fraction(3, 2), Fraction(7, 5)] +""" + +from fractions import Fraction + +from algorithm_lab._validation import integer + + +def continued_fraction_convergents(terms): + p0, p1, q0, q1 = 0, 1, 1, 0 + result = [] + for i, term in enumerate(terms): + integer(term, "term", minimum=1 if i else None) + p0, p1 = p1, term * p1 + p0 + q0, q1 = q1, term * q1 + q0 + result.append(Fraction(p1, q1)) + return result diff --git a/algorithm_lab/determinant.py b/algorithm_lab/determinant.py new file mode 100644 index 0000000..d9dbef4 --- /dev/null +++ b/algorithm_lab/determinant.py @@ -0,0 +1,36 @@ +"""Compute exact determinants by elimination. + +Input is a square matrix of int/Fraction values, excluding bool/float. +Return a Fraction; the empty determinant is 1. Singular matrices return 0. +First nonzero row pivoting is deterministic. O(n^3) rational operations and +O(n^2) copied storage; the input is unchanged. + +>>> determinant([[1, 2], [3, 4]]) +Fraction(-2, 1) +""" + +from fractions import Fraction + + +def determinant(matrix): + a = [list(row) for row in matrix] + n = len(a) + if any(len(row) != n for row in a): + raise ValueError("matrix must be square") + if any(isinstance(v, bool) or not isinstance(v, (int, Fraction)) for row in a for v in row): + raise ValueError("entries must be integers or Fractions") + a = [[Fraction(v) for v in row] for row in a] + result = Fraction(1) + for j in range(n): + pivot = next((i for i in range(j, n) if a[i][j]), None) + if pivot is None: + return Fraction(0) + if pivot != j: + a[j], a[pivot] = a[pivot], a[j] + result = -result + result *= a[j][j] + for i in range(j + 1, n): + factor = a[i][j] / a[j][j] + for k in range(j + 1, n): + a[i][k] -= factor * a[j][k] + return result diff --git a/algorithm_lab/divisor_sieve.py b/algorithm_lab/divisor_sieve.py new file mode 100644 index 0000000..150204f --- /dev/null +++ b/algorithm_lab/divisor_sieve.py @@ -0,0 +1,20 @@ +"""Compute every positive divisor list through a bound. + +Return lists indexed by integer 0..limit, each in ascending order; index 0 +is empty. limit is a nonnegative integer excluding bool. O(n log n) time +and output storage, where n = limit. + +>>> divisor_sieve(4) +[[], [1], [1, 2], [1, 3], [1, 2, 4]] +""" + +from algorithm_lab._validation import integer + + +def divisor_sieve(limit): + integer(limit, minimum=0) + result = [[] for _ in range(limit + 1)] + for divisor in range(1, limit + 1): + for multiple in range(divisor, limit + 1, divisor): + result[multiple].append(divisor) + return result diff --git a/algorithm_lab/euler_totient.py b/algorithm_lab/euler_totient.py new file mode 100644 index 0000000..321faf0 --- /dev/null +++ b/algorithm_lab/euler_totient.py @@ -0,0 +1,24 @@ +"""Count coprime residues by prime factorization. + +n is a positive integer, excluding bool. phi(1) is 1. Trial division takes +O(sqrt(n)) arithmetic steps and O(1) integer storage. + +>>> euler_totient(36) +12 +""" + +from algorithm_lab._validation import integer + + +def euler_totient(n): + integer(n, minimum=1) + result, remaining, factor = n, n, 2 + while factor * factor <= remaining: + if remaining % factor == 0: + result -= result // factor + while remaining % factor == 0: + remaining //= factor + factor += 1 + if remaining > 1: + result -= result // remaining + return result diff --git a/algorithm_lab/gaussian_elimination.py b/algorithm_lab/gaussian_elimination.py new file mode 100644 index 0000000..33ae0bf --- /dev/null +++ b/algorithm_lab/gaussian_elimination.py @@ -0,0 +1,38 @@ +"""Solve square rational linear systems by pivoted elimination. + +Accept a square matrix and matching right-hand side of int/Fraction values, +excluding bool/float. Return exact Fractions without changing inputs. Empty +systems return []; singular systems raise ValueError. Pivots choose the first +nonzero remaining row. O(n^3) rational operations, O(n^2) space. + +>>> gaussian_elimination([[2, 1], [1, -1]], [5, 1]) +[Fraction(2, 1), Fraction(1, 1)] +""" + +from fractions import Fraction + + +def gaussian_elimination(matrix, rhs): + rows, rhs = [list(row) for row in matrix], list(rhs) + n = len(rows) + if len(rhs) != n or any(len(row) != n for row in rows): + raise ValueError("a square matrix and matching rhs are required") + if any( + isinstance(v, bool) or not isinstance(v, (int, Fraction)) for row in rows for v in row + ) or any(isinstance(v, bool) or not isinstance(v, (int, Fraction)) for v in rhs): + raise ValueError("entries must be integers or Fractions") + a = [[Fraction(v) for v in row] + [Fraction(b)] for row, b in zip(rows, rhs, strict=True)] + for column in range(n): + pivot = next((i for i in range(column, n) if a[i][column]), None) + if pivot is None: + raise ValueError("matrix is singular") + a[column], a[pivot] = a[pivot], a[column] + scale = a[column][column] + a[column] = [value / scale for value in a[column]] + for i in range(column + 1, n): + factor = a[i][column] + a[i] = [v - factor * w for v, w in zip(a[i], a[column], strict=True)] + result = [Fraction(0)] * n + for i in range(n - 1, -1, -1): + result[i] = a[i][-1] - sum(a[i][j] * result[j] for j in range(i + 1, n)) + return result diff --git a/algorithm_lab/integer_nth_root.py b/algorithm_lab/integer_nth_root.py new file mode 100644 index 0000000..3dbc6ec --- /dev/null +++ b/algorithm_lab/integer_nth_root.py @@ -0,0 +1,28 @@ +"""Exact floor roots using integer binary search. + +value >= 0 and degree >= 1 are integers excluding bool. Return r satisfying +r**degree <= value < (r+1)**degree, without floats. O(bit_length(value)) +comparisons, each using integer exponentiation; O(1) big integers stored. + +>>> integer_nth_root(1000, 3) +10 +""" + +from algorithm_lab._validation import integer + + +def integer_nth_root(value, degree): + integer(value, minimum=0) + integer(degree, "degree", minimum=1) + if value < 2 or degree == 1: + return value + if degree >= value.bit_length(): + return 1 + low, high = 0, 1 << ((value.bit_length() + degree - 1) // degree) + while low + 1 < high: + middle = (low + high) // 2 + if middle**degree <= value: + low = middle + else: + high = middle + return high if high**degree <= value else low diff --git a/algorithm_lab/lagrange_interpolation.py b/algorithm_lab/lagrange_interpolation.py new file mode 100644 index 0000000..87e88a6 --- /dev/null +++ b/algorithm_lab/lagrange_interpolation.py @@ -0,0 +1,32 @@ +"""Evaluate the interpolating polynomial with exact rationals. + +Points and x contain int or Fraction values, excluding bool and float. +Require at least one point and distinct abscissas. Return a Fraction; the +polynomial has degree less than the number of points. O(n^2) rational +operations and O(n) space. Point order does not affect the answer. + +>>> lagrange_interpolation([(0, 1), (1, 2), (2, 5)], 3) +Fraction(10, 1) +""" + +from fractions import Fraction + + +def lagrange_interpolation(points, x): + def rational(value): + if isinstance(value, bool) or not isinstance(value, (int, Fraction)): + raise ValueError("coordinates must be integers or Fractions") + return Fraction(value) + + x = rational(x) + points = [(rational(a), rational(b)) for a, b in points] + if not points or len({a for a, _ in points}) != len(points): + raise ValueError("points must be nonempty with distinct abscissas") + result = Fraction(0) + for i, (a, b) in enumerate(points): + term = b + for j, (other, _) in enumerate(points): + if i != j: + term *= (x - other) / (a - other) + result += term + return result diff --git a/algorithm_lab/linear_recurrence.py b/algorithm_lab/linear_recurrence.py new file mode 100644 index 0000000..5d50e63 --- /dev/null +++ b/algorithm_lab/linear_recurrence.py @@ -0,0 +1,28 @@ +"""Evaluate integer recurrences with a companion matrix. + +initial and coefficients are nonempty equally sized integer sequences. +For k terms, a[n] = c[0]*a[n-1] + ... + c[k-1]*a[n-k]. n >= 0 is an +integer; bool is rejected throughout. O(k^3 log(n+1)) arithmetic operations +and O(k^2) space. Return an exact integer without modifying inputs. + +>>> linear_recurrence([0, 1], [1, 1], 10) +55 +""" + +from algorithm_lab._validation import integer +from algorithm_lab.matrix_power import matrix_power + + +def linear_recurrence(initial, coefficients, n): + integer(n, "n", minimum=0) + initial, coefficients = list(initial), list(coefficients) + k = len(initial) + if not k or len(coefficients) != k: + raise ValueError("initial values and coefficients must have the same positive length") + for value in initial + coefficients: + integer(value) + if n < k: + return initial[n] + matrix = [coefficients] + [[int(j == i - 1) for j in range(k)] for i in range(1, k)] + power = matrix_power(matrix, n - k + 1) + return sum(power[0][j] * initial[k - 1 - j] for j in range(k)) diff --git a/algorithm_lab/matrix_power.py b/algorithm_lab/matrix_power.py new file mode 100644 index 0000000..7372586 --- /dev/null +++ b/algorithm_lab/matrix_power.py @@ -0,0 +1,37 @@ +"""Integer matrix powers by repeated squaring. + +The matrix must be square with integer entries; exponent is a nonnegative +integer. Booleans are rejected. Return a new matrix; exponent 0 returns the +identity (including [] for the empty matrix). O(n^3 log(exponent+1)) integer +operations and O(n^2) storage. + +>>> matrix_power([[1, 1], [1, 0]], 5) +[[8, 5], [5, 3]] +""" + +from algorithm_lab._validation import integer + + +def matrix_power(matrix, exponent): + integer(exponent, "exponent", minimum=0) + a = [list(row) for row in matrix] + n = len(a) + if any(len(row) != n for row in a): + raise ValueError("matrix must be square") + for row in a: + for value in row: + integer(value, "entry") + + def multiply(left, right): + return [ + [sum(left[i][k] * right[k][j] for k in range(n)) for j in range(n)] for i in range(n) + ] + + result = [[int(i == j) for j in range(n)] for i in range(n)] + while exponent: + if exponent & 1: + result = multiply(result, a) + exponent //= 2 + if exponent: + a = multiply(a, a) + return result diff --git a/algorithm_lab/mobius_sieve.py b/algorithm_lab/mobius_sieve.py new file mode 100644 index 0000000..ba5e1eb --- /dev/null +++ b/algorithm_lab/mobius_sieve.py @@ -0,0 +1,30 @@ +"""Linear sieve of the Mobius function. + +Return mu(0)..mu(limit), using 0 as the unused mu(0) sentinel. The positive +values are 0 for square factors and (-1)^k for k distinct prime factors. +limit is a nonnegative integer excluding bool. O(limit) time and space. + +>>> mobius_sieve(6) +[0, 1, -1, -1, 0, -1, 1] +""" + +from algorithm_lab._validation import integer + + +def mobius_sieve(limit): + integer(limit, minimum=0) + mu, composite, primes = [0] * (limit + 1), [False] * (limit + 1), [] + if limit: + mu[1] = 1 + for n in range(2, limit + 1): + if not composite[n]: + primes.append(n) + mu[n] = -1 + for prime in primes: + if n * prime > limit: + break + composite[n * prime] = True + if n % prime == 0: + break + mu[n * prime] = -mu[n] + return mu diff --git a/algorithm_lab/modular_inverse.py b/algorithm_lab/modular_inverse.py new file mode 100644 index 0000000..531a8d6 --- /dev/null +++ b/algorithm_lab/modular_inverse.py @@ -0,0 +1,24 @@ +"""Extended-Euclidean modular inverses. + +Accept signed integers and modulus >= 2; reject booleans and non-coprime +inputs. Return the unique inverse in [0, modulus). O(log modulus) Euclidean +steps and O(1) integers of auxiliary storage. + +>>> modular_inverse(-3, 11) +7 +""" + +from algorithm_lab._validation import integer + + +def modular_inverse(value, modulus): + integer(value) + integer(modulus, "modulus", minimum=2) + old_r, r, old_s, s = modulus, value % modulus, 0, 1 + while r: + quotient = old_r // r + old_r, r = r, old_r - quotient * r + old_s, s = s, old_s - quotient * s + if old_r != 1: + raise ValueError("value and modulus must be coprime") + return old_s % modulus diff --git a/algorithm_lab/multiplicative_order.py b/algorithm_lab/multiplicative_order.py new file mode 100644 index 0000000..cec5d73 --- /dev/null +++ b/algorithm_lab/multiplicative_order.py @@ -0,0 +1,27 @@ +"""Find the order of an invertible residue. + +value is an integer and modulus >= 2, excluding bool. Reject non-coprime +inputs. Return the smallest positive k with value**k == 1 modulo modulus. +This educational repeated-multiplication method takes O(modulus) modular +multiplications and O(1) integers of storage. + +>>> multiplicative_order(2, 7) +3 +""" + +from math import gcd + +from algorithm_lab._validation import integer + + +def multiplicative_order(value, modulus): + integer(value) + integer(modulus, "modulus", minimum=2) + if gcd(value, modulus) != 1: + raise ValueError("value and modulus must be coprime") + residue, order = value % modulus, 1 + current = residue + while current != 1: + current = current * residue % modulus + order += 1 + return order diff --git a/algorithm_lab/multiset_permutations.py b/algorithm_lab/multiset_permutations.py new file mode 100644 index 0000000..17b9bac --- /dev/null +++ b/algorithm_lab/multiset_permutations.py @@ -0,0 +1,31 @@ +"""Generate distinct integer permutations in lexicographic order. + +Accept an iterable of integers excluding bool. Yield tuples in sorted order +without repeating permutations of equal elements. Empty input yields (). +Successor generation avoids recursion and uses O(n) working space and O(n) +time per output after O(n log n) sorting; input is not modified. + +>>> list(multiset_permutations([2, 1, 1])) +[(1, 1, 2), (1, 2, 1), (2, 1, 1)] +""" + +from algorithm_lab._validation import integer + + +def multiset_permutations(values): + values = list(values) + for value in values: + integer(value) + values.sort() + while True: + yield tuple(values) + i = len(values) - 2 + while i >= 0 and values[i] >= values[i + 1]: + i -= 1 + if i < 0: + return + j = len(values) - 1 + while values[j] <= values[i]: + j -= 1 + values[i], values[j] = values[j], values[i] + values[i + 1 :] = reversed(values[i + 1 :]) diff --git a/algorithm_lab/permutation_rank.py b/algorithm_lab/permutation_rank.py new file mode 100644 index 0000000..5d25eac --- /dev/null +++ b/algorithm_lab/permutation_rank.py @@ -0,0 +1,26 @@ +"""Lehmer-code ranking of permutations of range(n). + +Input must contain each integer in range(n) once, excluding bool. Return the +zero-based lexicographic rank, with the empty permutation ranked 0. O(n^2) +time and O(n) storage using a sorted list of remaining values. + +>>> permutation_rank([2, 0, 1]) +4 +""" + +from algorithm_lab._validation import integer + + +def permutation_rank(permutation): + values = list(permutation) + for value in values: + integer(value) + n = len(values) + if set(values) != set(range(n)): + raise ValueError("input must be a permutation of range(n)") + remaining, rank = list(range(n)), 0 + for value in values: + position = remaining.index(value) + rank = rank * len(remaining) + position + remaining.pop(position) + return rank diff --git a/algorithm_lab/permutation_unrank.py b/algorithm_lab/permutation_unrank.py new file mode 100644 index 0000000..6e2ecc0 --- /dev/null +++ b/algorithm_lab/permutation_unrank.py @@ -0,0 +1,27 @@ +"""Factoradic selection of a lexicographic permutation. + +n and rank are nonnegative integers excluding bool, with rank < n!. +Return the permutation of range(n) at that zero-based lexicographic rank. +O(n^2) time and O(n) space; n=0 admits only rank=0 and returns []. + +>>> permutation_unrank(3, 4) +[2, 0, 1] +""" + +from math import factorial + +from algorithm_lab._validation import integer + + +def permutation_unrank(n, rank): + integer(n, "n", minimum=0) + integer(rank, "rank", minimum=0) + total = factorial(n) + if rank >= total: + raise ValueError("permutation rank is out of range") + remaining, result = list(range(n)), [] + for size in range(n, 0, -1): + total //= size + position, rank = divmod(rank, total) + result.append(remaining.pop(position)) + return result diff --git a/algorithm_lab/polynomial_divmod.py b/algorithm_lab/polynomial_divmod.py new file mode 100644 index 0000000..4c4b562 --- /dev/null +++ b/algorithm_lab/polynomial_divmod.py @@ -0,0 +1,38 @@ +"""Exact polynomial long division over rational coefficients. + +Inputs contain int or Fraction coefficients in increasing degree order; +bool and floats are rejected. Return (quotient, remainder), both canonical +Fraction lists with [] representing zero. A zero divisor raises ValueError. +O(n*m) rational operations and O(n+m) space for degrees n and m. + +>>> polynomial_divmod([-1, 0, 1], [-1, 1]) +([Fraction(1, 1), Fraction(1, 1)], []) +""" + +from fractions import Fraction + + +def polynomial_divmod(dividend, divisor): + def convert(values): + result = [] + for value in values: + if isinstance(value, bool) or not isinstance(value, (int, Fraction)): + raise ValueError("coefficients must be integers or Fractions") + result.append(Fraction(value)) + while result and result[-1] == 0: + result.pop() + return result + + remainder, divisor = convert(dividend), convert(divisor) + if not divisor: + raise ValueError("divisor must be nonzero") + quotient = [Fraction(0)] * max(0, len(remainder) - len(divisor) + 1) + while remainder and len(remainder) >= len(divisor): + shift = len(remainder) - len(divisor) + factor = remainder[-1] / divisor[-1] + quotient[shift] = factor + for i, coefficient in enumerate(divisor): + remainder[i + shift] -= factor * coefficient + while remainder and remainder[-1] == 0: + remainder.pop() + return quotient, remainder diff --git a/algorithm_lab/polynomial_evaluate.py b/algorithm_lab/polynomial_evaluate.py new file mode 100644 index 0000000..9faf39d --- /dev/null +++ b/algorithm_lab/polynomial_evaluate.py @@ -0,0 +1,22 @@ +"""Horner evaluation of integer polynomials. + +Coefficients are integers in increasing degree order; x is an integer. +Booleans are rejected. An empty polynomial evaluates to 0. O(n) arithmetic +steps and O(n) copied storage; input is not modified. + +>>> polynomial_evaluate([1, -2, 3], 4) +41 +""" + +from algorithm_lab._validation import integer + + +def polynomial_evaluate(coefficients, x): + integer(x, "x") + values = list(coefficients) + for coefficient in values: + integer(coefficient, "coefficient") + result = 0 + for coefficient in reversed(values): + result = result * x + coefficient + return result diff --git a/algorithm_lab/polynomial_multiply.py b/algorithm_lab/polynomial_multiply.py new file mode 100644 index 0000000..5ab4396 --- /dev/null +++ b/algorithm_lab/polynomial_multiply.py @@ -0,0 +1,26 @@ +"""Exact dense polynomial convolution. + +Inputs contain integers in increasing degree order, excluding bool. +Return canonical coefficients with trailing zeros removed; zero is []. +Inputs are copied. O(n*m) arithmetic steps and O(n+m) space. + +>>> polynomial_multiply([1, 2], [-1, 3]) +[-1, 1, 6] +""" + +from algorithm_lab._validation import integer + + +def polynomial_multiply(left, right): + left, right = list(left), list(right) + for value in left + right: + integer(value, "coefficient") + if not left or not right: + return [] + result = [0] * (len(left) + len(right) - 1) + for i, a in enumerate(left): + for j, b in enumerate(right): + result[i + j] += a * b + while result and result[-1] == 0: + result.pop() + return result diff --git a/algorithm_lab/segmented_sieve.py b/algorithm_lab/segmented_sieve.py new file mode 100644 index 0000000..ecae2f5 --- /dev/null +++ b/algorithm_lab/segmented_sieve.py @@ -0,0 +1,33 @@ +"""Find primes in a half-open interval with segmented marking. + +Integer bounds satisfy 0 <= start <= stop, excluding bool. Return primes +in ascending order, including start and excluding stop. With n = stop and +w = stop-start, storage is O(sqrt(n)+w); time is +O(sqrt(n) log log(n+2) + w log log(n+2)). + +>>> segmented_sieve(10, 20) +[11, 13, 17, 19] +""" + +from math import isqrt + +from algorithm_lab._validation import integer + + +def segmented_sieve(start, stop): + integer(start, "start", minimum=0) + integer(stop, "stop", minimum=start) + if stop <= 2 or start == stop: + return [] + low = max(2, start) + limit = isqrt(stop - 1) + base = [True] * (limit + 1) + segment = [True] * (stop - low) + for prime in range(2, limit + 1): + if base[prime]: + for multiple in range(prime * prime, limit + 1, prime): + base[multiple] = False + first = max(prime * prime, ((low + prime - 1) // prime) * prime) + for multiple in range(first, stop, prime): + segment[multiple - low] = False + return [low + offset for offset, prime in enumerate(segment) if prime] diff --git a/algorithm_lab/stern_brocot_path.py b/algorithm_lab/stern_brocot_path.py new file mode 100644 index 0000000..8a7da8d --- /dev/null +++ b/algorithm_lab/stern_brocot_path.py @@ -0,0 +1,28 @@ +"""Run-length paths to positive rationals in the Stern-Brocot tree. + +Numerator and denominator are positive integers excluding bool. Return +(direction, count) runs from 1/1, where L decreases and R increases the +mediant. Equivalent unreduced fractions have the same path. Euclidean +division avoids expanding long runs: O(log(max(n,d))) steps and storage. + +>>> stern_brocot_path(5, 2) +[('R', 2), ('L', 1)] +""" + +from algorithm_lab._validation import integer + + +def stern_brocot_path(numerator, denominator): + integer(numerator, "numerator", minimum=1) + integer(denominator, "denominator", minimum=1) + result = [] + while numerator != denominator: + if numerator < denominator: + count = (denominator - 1) // numerator + result.append(("L", count)) + denominator -= count * numerator + else: + count = (numerator - 1) // denominator + result.append(("R", count)) + numerator -= count * denominator + return result diff --git a/algorithm_lab/stirling_second.py b/algorithm_lab/stirling_second.py new file mode 100644 index 0000000..87254cf --- /dev/null +++ b/algorithm_lab/stirling_second.py @@ -0,0 +1,24 @@ +"""Count partitions into a specified number of nonempty blocks. + +n and k are nonnegative integers excluding bool. S(0,0)=1, and S(n,k)=0 +when k>n. Rolling dynamic programming uses O(n*min(n,k)) arithmetic steps +and O(min(n,k)) space; all counts are exact integers. + +>>> stirling_second(5, 2) +15 +""" + +from algorithm_lab._validation import integer + + +def stirling_second(n, k): + integer(n, "n", minimum=0) + integer(k, "k", minimum=0) + if k > n: + return 0 + row = [1] + [0] * k + for i in range(1, n + 1): + for j in range(min(i, k), 0, -1): + row[j] = j * row[j] + row[j - 1] + row[0] = 0 + return row[k] diff --git a/algorithm_lab/subset_mobius_transform.py b/algorithm_lab/subset_mobius_transform.py new file mode 100644 index 0000000..048b7cf --- /dev/null +++ b/algorithm_lab/subset_mobius_transform.py @@ -0,0 +1,27 @@ +"""Invert subset sums by Mobius inversion on bit masks. + +Input is a nonempty integer array of power-of-two length, excluding bool. +Return the inverse of the subset zeta transform, leaving input unchanged. +O(n log n) integer operations and O(n) storage. + +>>> subset_mobius_transform([1, 3, 4, 10]) +[1, 2, 3, 4] +""" + +from algorithm_lab._validation import integer + + +def subset_mobius_transform(values): + result = list(values) + n = len(result) + if not n or n & (n - 1): + raise ValueError("length must be a positive power of two") + for value in result: + integer(value) + bit = 1 + while bit < n: + for mask in range(n): + if mask & bit: + result[mask] -= result[mask ^ bit] + bit <<= 1 + return result diff --git a/algorithm_lab/subset_zeta_transform.py b/algorithm_lab/subset_zeta_transform.py new file mode 100644 index 0000000..b3cc0c9 --- /dev/null +++ b/algorithm_lab/subset_zeta_transform.py @@ -0,0 +1,27 @@ +"""Sum values over every subset of each bit mask. + +Input is a nonempty integer array of power-of-two length, excluding bool. +Return result[mask] = sum(values[sub] for sub subset-of mask). The input +is unchanged. O(n log n) integer operations and O(n) copied storage. + +>>> subset_zeta_transform([1, 2, 3, 4]) +[1, 3, 4, 10] +""" + +from algorithm_lab._validation import integer + + +def subset_zeta_transform(values): + result = list(values) + n = len(result) + if not n or n & (n - 1): + raise ValueError("length must be a positive power of two") + for value in result: + integer(value) + bit = 1 + while bit < n: + for mask in range(n): + if mask & bit: + result[mask] += result[mask ^ bit] + bit <<= 1 + return result diff --git a/tests/test_bounded_compositions.py b/tests/test_bounded_compositions.py new file mode 100644 index 0000000..65f844d --- /dev/null +++ b/tests/test_bounded_compositions.py @@ -0,0 +1,17 @@ +import unittest +from itertools import product + +from algorithm_lab.bounded_compositions import bounded_compositions + + +class Tests(unittest.TestCase): + def test_cartesian_oracle(self): + for bounds in [[], [0], [3], [2, 1, 3], [0, 2, 0, 2]]: + for total in range(9): + expected = [p for p in product(*(range(b + 1) for b in bounds)) if sum(p) == total] + self.assertEqual(list(bounded_compositions(total, bounds)), expected) + + def test_invalid(self): + for total, bounds in [(-1, []), (1, [-1]), (True, []), (1, [False])]: + with self.assertRaises(ValueError): + list(bounded_compositions(total, bounds)) diff --git a/tests/test_combination_rank.py b/tests/test_combination_rank.py new file mode 100644 index 0000000..6af76ff --- /dev/null +++ b/tests/test_combination_rank.py @@ -0,0 +1,17 @@ +import unittest +from itertools import combinations + +from algorithm_lab.combination_rank import combination_rank + + +class Tests(unittest.TestCase): + def test_enumeration_oracle(self): + for n in range(9): + for k in range(n + 1): + for rank, values in enumerate(combinations(range(n), k)): + self.assertEqual(combination_rank(n, values), rank) + + def test_invalid(self): + for n, values in [(3, [1, 1]), (3, [2, 1]), (3, [3]), (3, [True]), (-1, [])]: + with self.assertRaises(ValueError): + combination_rank(n, values) diff --git a/tests/test_combination_unrank.py b/tests/test_combination_unrank.py new file mode 100644 index 0000000..0a57f23 --- /dev/null +++ b/tests/test_combination_unrank.py @@ -0,0 +1,17 @@ +import unittest +from itertools import combinations + +from algorithm_lab.combination_unrank import combination_unrank + + +class Tests(unittest.TestCase): + def test_enumeration_oracle(self): + for n in range(9): + for k in range(n + 1): + for rank, values in enumerate(combinations(range(n), k)): + self.assertEqual(combination_unrank(n, k, rank), list(values)) + + def test_invalid(self): + for args in [(2, 3, 0), (2, 1, 2), (2, 1, -1), (True, 1, 0), (2, False, 0)]: + with self.assertRaises(ValueError): + combination_unrank(*args) diff --git a/tests/test_continued_fraction.py b/tests/test_continued_fraction.py new file mode 100644 index 0000000..ca6d1a6 --- /dev/null +++ b/tests/test_continued_fraction.py @@ -0,0 +1,25 @@ +import unittest +from fractions import Fraction + +from algorithm_lab.continued_fraction import continued_fraction + + +class Tests(unittest.TestCase): + def test_reconstruct_signed_rationals(self): + for n in range(-25, 26): + for d in range(-12, 13): + if d == 0: + continue + terms = continued_fraction(n, d) + value = Fraction(terms[-1]) + for term in reversed(terms[:-1]): + value = term + 1 / value + self.assertEqual(value, Fraction(n, d)) + self.assertTrue(all(x > 0 for x in terms[1:])) + if len(terms) > 1: + self.assertGreater(terms[-1], 1) + + def test_invalid(self): + for args in [(1, 0), (True, 1), (1, 2.5)]: + with self.assertRaises(ValueError): + continued_fraction(*args) diff --git a/tests/test_continued_fraction_convergents.py b/tests/test_continued_fraction_convergents.py new file mode 100644 index 0000000..ed701ce --- /dev/null +++ b/tests/test_continued_fraction_convergents.py @@ -0,0 +1,20 @@ +import unittest +from fractions import Fraction + +from algorithm_lab.continued_fraction_convergents import continued_fraction_convergents + + +class Tests(unittest.TestCase): + def test_prefix_oracle(self): + for terms in [[], [-3], [0, 2], [3, 7, 15, 1, 292], [-2, 1, 3, 2]]: + result = continued_fraction_convergents(iter(terms)) + for i, actual in enumerate(result): + expected = Fraction(terms[i]) + for term in reversed(terms[:i]): + expected = term + 1 / expected + self.assertEqual(actual, expected) + + def test_invalid(self): + for terms in [[True], [1, 0], [1, -2], [1, 2.0]]: + with self.assertRaises(ValueError): + continued_fraction_convergents(terms) diff --git a/tests/test_determinant.py b/tests/test_determinant.py new file mode 100644 index 0000000..584bbc2 --- /dev/null +++ b/tests/test_determinant.py @@ -0,0 +1,24 @@ +import itertools +import math +import random +import unittest + +from algorithm_lab.determinant import determinant + + +class Tests(unittest.TestCase): + def test_leibniz_oracle(self): + rng = random.Random(217) + for n in range(5): + for _ in range(15): + a = [[rng.randrange(-3, 4) for _ in range(n)] for _ in range(n)] + expected = 0 + for p in itertools.permutations(range(n)): + inversions = sum(p[i] > p[j] for i in range(n) for j in range(i + 1, n)) + expected += (-1) ** inversions * math.prod(a[i][p[i]] for i in range(n)) + self.assertEqual(determinant(a), expected) + + def test_invalid(self): + for a in [[[1, 2]], [[True]], [[1.0]]]: + with self.assertRaises(ValueError): + determinant(a) diff --git a/tests/test_divisor_sieve.py b/tests/test_divisor_sieve.py new file mode 100644 index 0000000..ed9fe09 --- /dev/null +++ b/tests/test_divisor_sieve.py @@ -0,0 +1,18 @@ +import unittest + +from algorithm_lab.divisor_sieve import divisor_sieve + + +class Tests(unittest.TestCase): + def test_trial_division_oracle(self): + result = divisor_sieve(200) + for n in range(1, 201): + self.assertEqual(result[n], [d for d in range(1, n + 1) if n % d == 0]) + self.assertEqual(divisor_sieve(0), [[]]) + result[1].append(99) + self.assertNotIn(99, result[2]) + + def test_invalid(self): + for n in [-1, True, "3"]: + with self.assertRaises(ValueError): + divisor_sieve(n) diff --git a/tests/test_euler_totient.py b/tests/test_euler_totient.py new file mode 100644 index 0000000..15a9e6e --- /dev/null +++ b/tests/test_euler_totient.py @@ -0,0 +1,15 @@ +import math +import unittest + +from algorithm_lab.euler_totient import euler_totient + + +class Tests(unittest.TestCase): + def test_count_oracle(self): + for n in range(1, 400): + self.assertEqual(euler_totient(n), sum(math.gcd(n, k) == 1 for k in range(1, n + 1))) + + def test_invalid(self): + for n in [0, -1, True, 1.5]: + with self.assertRaises(ValueError): + euler_totient(n) diff --git a/tests/test_gaussian_elimination.py b/tests/test_gaussian_elimination.py new file mode 100644 index 0000000..ff4ced2 --- /dev/null +++ b/tests/test_gaussian_elimination.py @@ -0,0 +1,22 @@ +import unittest +from fractions import Fraction + +from algorithm_lab.gaussian_elimination import gaussian_elimination + + +class Tests(unittest.TestCase): + def test_known_solutions_and_row_swap(self): + for matrix, solution in [ + ([[0, 2], [3, 4]], [Fraction(1, 3), -7]), + ([[1, 2, 3], [0, 1, 4], [5, 6, 0]], [2, -1, 3]), + ]: + before = [row[:] for row in matrix] + rhs = [sum(a * b for a, b in zip(row, solution, strict=True)) for row in matrix] + self.assertEqual(gaussian_elimination(matrix, rhs), solution) + self.assertEqual(matrix, before) + self.assertEqual(gaussian_elimination([], []), []) + + def test_invalid(self): + for a, b in [([[1, 2], [2, 4]], [1, 2]), ([[1, 2]], [1]), ([[True]], [1]), ([[1]], [])]: + with self.assertRaises(ValueError): + gaussian_elimination(a, b) diff --git a/tests/test_integer_nth_root.py b/tests/test_integer_nth_root.py new file mode 100644 index 0000000..f9561aa --- /dev/null +++ b/tests/test_integer_nth_root.py @@ -0,0 +1,18 @@ +import unittest + +from algorithm_lab.integer_nth_root import integer_nth_root + + +class Tests(unittest.TestCase): + def test_bracketing(self): + for degree in range(1, 12): + for value in [*range(100), 10**300 - 1, 10**300, 10**300 + 1]: + root = integer_nth_root(value, degree) + self.assertLessEqual(root**degree, value) + self.assertGreater((root + 1) ** degree, value) + self.assertEqual(integer_nth_root(2, 10**20), 1) + + def test_invalid(self): + for args in [(-1, 2), (1, 0), (True, 2), (1, 2.0)]: + with self.assertRaises(ValueError): + integer_nth_root(*args) diff --git a/tests/test_lagrange_interpolation.py b/tests/test_lagrange_interpolation.py new file mode 100644 index 0000000..4987a1d --- /dev/null +++ b/tests/test_lagrange_interpolation.py @@ -0,0 +1,21 @@ +import unittest +from fractions import Fraction + +from algorithm_lab.lagrange_interpolation import lagrange_interpolation + + +class Tests(unittest.TestCase): + def test_recover_polynomials(self): + for coefficients in [[3], [1, -2], [4, 0, -3, 2]]: + + def evaluate(x, coefficients=coefficients): + return sum(c * x**i for i, c in enumerate(coefficients)) + + points = [(x, evaluate(x)) for x in range(len(coefficients))] + for x in [Fraction(1, 2), -5, 9]: + self.assertEqual(lagrange_interpolation(reversed(points), x), evaluate(x)) + + def test_invalid(self): + for points in [[], [(1, 2), (1, 3)], [(True, 2)], [(1, 2.0)]]: + with self.assertRaises(ValueError): + lagrange_interpolation(points, 0) diff --git a/tests/test_linear_recurrence.py b/tests/test_linear_recurrence.py new file mode 100644 index 0000000..04dd040 --- /dev/null +++ b/tests/test_linear_recurrence.py @@ -0,0 +1,22 @@ +import random +import unittest + +from algorithm_lab.linear_recurrence import linear_recurrence + + +class Tests(unittest.TestCase): + def test_direct_recurrence_oracle(self): + rng = random.Random(219) + for k in range(1, 6): + initial = [rng.randrange(-3, 4) for _ in range(k)] + coefficients = [rng.randrange(-2, 3) for _ in range(k)] + direct = initial[:] + for n in range(k, 30): + direct.append(sum(c * direct[n - j - 1] for j, c in enumerate(coefficients))) + for n, expected in enumerate(direct): + self.assertEqual(linear_recurrence(initial, coefficients, n), expected) + + def test_invalid(self): + for a, c, n in [([], [], 0), ([1], [1, 2], 0), ([True], [1], 0), ([1], [1], -1)]: + with self.assertRaises(ValueError): + linear_recurrence(a, c, n) diff --git a/tests/test_matrix_power.py b/tests/test_matrix_power.py new file mode 100644 index 0000000..15fdd84 --- /dev/null +++ b/tests/test_matrix_power.py @@ -0,0 +1,19 @@ +import unittest + +from algorithm_lab.matrix_power import matrix_power + + +class Tests(unittest.TestCase): + def test_known_powers(self): + for n in range(20): + self.assertEqual(matrix_power([[1, 3], [0, 1]], n), [[1, 3 * n], [0, 1]]) + self.assertEqual(matrix_power([[2, 0], [0, -3]], n), [[2**n, 0], [0, (-3) ** n]]) + self.assertEqual(matrix_power([], 10), []) + a = [[1, 2], [3, 4]] + matrix_power(a, 5) + self.assertEqual(a, [[1, 2], [3, 4]]) + + def test_invalid(self): + for a, n in [([[1, 2]], 2), ([[1]], -1), ([[True]], 1), ([[1]], False)]: + with self.assertRaises(ValueError): + matrix_power(a, n) diff --git a/tests/test_mobius_sieve.py b/tests/test_mobius_sieve.py new file mode 100644 index 0000000..3974f1e --- /dev/null +++ b/tests/test_mobius_sieve.py @@ -0,0 +1,16 @@ +import unittest + +from algorithm_lab.mobius_sieve import mobius_sieve + + +class Tests(unittest.TestCase): + def test_divisor_identity(self): + values = mobius_sieve(500) + self.assertEqual(mobius_sieve(0), [0]) + for n in range(1, 501): + self.assertEqual(sum(values[d] for d in range(1, n + 1) if n % d == 0), int(n == 1)) + + def test_invalid(self): + for n in [-1, True, 2.5]: + with self.assertRaises(ValueError): + mobius_sieve(n) diff --git a/tests/test_modular_inverse.py b/tests/test_modular_inverse.py new file mode 100644 index 0000000..2f5282a --- /dev/null +++ b/tests/test_modular_inverse.py @@ -0,0 +1,20 @@ +import unittest + +from algorithm_lab.modular_inverse import modular_inverse + + +class Tests(unittest.TestCase): + def test_exhaustive_oracle(self): + for modulus in range(2, 40): + for value in range(-40, 40): + candidates = [x for x in range(modulus) if value * x % modulus == 1] + if candidates: + self.assertEqual(modular_inverse(value, modulus), candidates[0]) + else: + with self.assertRaises(ValueError): + modular_inverse(value, modulus) + + def test_invalid(self): + for args in [(True, 3), (2, 1), (2, 3.0)]: + with self.assertRaises(ValueError): + modular_inverse(*args) diff --git a/tests/test_multiplicative_order.py b/tests/test_multiplicative_order.py new file mode 100644 index 0000000..3dfcfd5 --- /dev/null +++ b/tests/test_multiplicative_order.py @@ -0,0 +1,21 @@ +import unittest +from math import gcd + +from algorithm_lab.multiplicative_order import multiplicative_order + + +class Tests(unittest.TestCase): + def test_minimal_exponent(self): + for modulus in range(2, 60): + for value in range(-10, 30): + if gcd(value, modulus) == 1: + expected = next(k for k in range(1, modulus) if pow(value, k, modulus) == 1) + self.assertEqual(multiplicative_order(value, modulus), expected) + else: + with self.assertRaises(ValueError): + multiplicative_order(value, modulus) + + def test_invalid(self): + for args in [(1, 1), (True, 3), (1, 3.5)]: + with self.assertRaises(ValueError): + multiplicative_order(*args) diff --git a/tests/test_multiset_permutations.py b/tests/test_multiset_permutations.py new file mode 100644 index 0000000..4659a14 --- /dev/null +++ b/tests/test_multiset_permutations.py @@ -0,0 +1,19 @@ +import unittest +from itertools import permutations, product + +from algorithm_lab.multiset_permutations import multiset_permutations + + +class Tests(unittest.TestCase): + def test_deduplicated_oracle(self): + for size in range(6): + for values in product(range(2), repeat=size): + self.assertEqual( + list(multiset_permutations(values)), sorted(set(permutations(values))) + ) + self.assertEqual(next(multiset_permutations([1] * 2000)), (1,) * 2000) + + def test_invalid(self): + for values in [[True], [1.5]]: + with self.assertRaises(ValueError): + list(multiset_permutations(values)) diff --git a/tests/test_permutation_rank.py b/tests/test_permutation_rank.py new file mode 100644 index 0000000..709be7a --- /dev/null +++ b/tests/test_permutation_rank.py @@ -0,0 +1,16 @@ +import unittest +from itertools import permutations + +from algorithm_lab.permutation_rank import permutation_rank + + +class Tests(unittest.TestCase): + def test_enumeration_oracle(self): + for n in range(7): + for rank, values in enumerate(permutations(range(n))): + self.assertEqual(permutation_rank(values), rank) + + def test_invalid(self): + for values in [[0, 0], [1], [-1, 0], [True, 0], [0.0]]: + with self.assertRaises(ValueError): + permutation_rank(values) diff --git a/tests/test_permutation_unrank.py b/tests/test_permutation_unrank.py new file mode 100644 index 0000000..da1e12b --- /dev/null +++ b/tests/test_permutation_unrank.py @@ -0,0 +1,16 @@ +import unittest +from itertools import permutations + +from algorithm_lab.permutation_unrank import permutation_unrank + + +class Tests(unittest.TestCase): + def test_enumeration_oracle(self): + for n in range(7): + for rank, values in enumerate(permutations(range(n))): + self.assertEqual(permutation_unrank(n, rank), list(values)) + + def test_invalid(self): + for args in [(0, 1), (3, 6), (2, -1), (-1, 0), (True, 0)]: + with self.assertRaises(ValueError): + permutation_unrank(*args) diff --git a/tests/test_polynomial_divmod.py b/tests/test_polynomial_divmod.py new file mode 100644 index 0000000..4603423 --- /dev/null +++ b/tests/test_polynomial_divmod.py @@ -0,0 +1,27 @@ +import random +import unittest +from fractions import Fraction + +from algorithm_lab.polynomial_divmod import polynomial_divmod + + +class Tests(unittest.TestCase): + def test_division_identity(self): + rng = random.Random(213) + for _ in range(100): + a = [rng.randrange(-5, 6) for _ in range(rng.randrange(10))] + b = [rng.randrange(-5, 6) for _ in range(rng.randrange(5))] + [2] + q, r = polynomial_divmod(a, b) + self.assertLess(len(r), len(b)) + for x in range(-4, 5): + + def evaluate(values, x=x): + return sum(c * x**i for i, c in enumerate(values)) + + self.assertEqual(evaluate(a), evaluate(q) * evaluate(b) + evaluate(r)) + self.assertEqual(polynomial_divmod([1], [2]), ([Fraction(1, 2)], [])) + + def test_invalid(self): + for a, b in [([1], []), ([1], [0, 0]), ([True], [1]), ([1.0], [1])]: + with self.assertRaises(ValueError): + polynomial_divmod(a, b) diff --git a/tests/test_polynomial_evaluate.py b/tests/test_polynomial_evaluate.py new file mode 100644 index 0000000..2fc1ff7 --- /dev/null +++ b/tests/test_polynomial_evaluate.py @@ -0,0 +1,21 @@ +import random +import unittest + +from algorithm_lab.polynomial_evaluate import polynomial_evaluate + + +class Tests(unittest.TestCase): + def test_power_sum_oracle(self): + rng = random.Random(211) + for _ in range(100): + values = [rng.randrange(-10, 11) for _ in range(rng.randrange(12))] + for x in [-100, -1, 0, 1, 100]: + self.assertEqual( + polynomial_evaluate(iter(values), x), + sum(c * x**i for i, c in enumerate(values)), + ) + + def test_invalid(self): + for values, x in [([True], 1), ([1.5], 2), ([1], False)]: + with self.assertRaises(ValueError): + polynomial_evaluate(values, x) diff --git a/tests/test_polynomial_multiply.py b/tests/test_polynomial_multiply.py new file mode 100644 index 0000000..74ca29f --- /dev/null +++ b/tests/test_polynomial_multiply.py @@ -0,0 +1,24 @@ +import random +import unittest + +from algorithm_lab.polynomial_multiply import polynomial_multiply + + +class Tests(unittest.TestCase): + def test_evaluation_identity(self): + rng = random.Random(212) + for _ in range(80): + a = [rng.randrange(-4, 5) for _ in range(rng.randrange(8))] + b = [rng.randrange(-4, 5) for _ in range(rng.randrange(8))] + result = polynomial_multiply(a, b) + for x in range(-4, 5): + + def evaluate(values, x=x): + return sum(c * x**i for i, c in enumerate(values)) + + self.assertEqual(evaluate(result), evaluate(a) * evaluate(b)) + self.assertTrue(not result or result[-1] != 0) + + def test_invalid(self): + with self.assertRaises(ValueError): + polynomial_multiply([], [True]) diff --git a/tests/test_segmented_sieve.py b/tests/test_segmented_sieve.py new file mode 100644 index 0000000..7c68c03 --- /dev/null +++ b/tests/test_segmented_sieve.py @@ -0,0 +1,22 @@ +import math +import unittest + +from algorithm_lab.segmented_sieve import segmented_sieve + + +class Tests(unittest.TestCase): + def test_all_small_intervals(self): + for start in range(30): + for stop in range(start, 60): + expected = [ + n + for n in range(max(2, start), stop) + if all(n % d for d in range(2, math.isqrt(n) + 1)) + ] + self.assertEqual(segmented_sieve(start, stop), expected) + self.assertEqual(segmented_sieve(49, 50), []) + + def test_invalid(self): + for args in [(-1, 3), (4, 3), (True, 3), (1, 3.5)]: + with self.assertRaises(ValueError): + segmented_sieve(*args) diff --git a/tests/test_stern_brocot_path.py b/tests/test_stern_brocot_path.py new file mode 100644 index 0000000..e0014f2 --- /dev/null +++ b/tests/test_stern_brocot_path.py @@ -0,0 +1,25 @@ +import unittest +from fractions import Fraction + +from algorithm_lab.stern_brocot_path import stern_brocot_path + + +class Tests(unittest.TestCase): + def test_walk_tree(self): + for n in range(1, 20): + for d in range(1, 20): + left, right = (0, 1), (1, 0) + for direction, count in stern_brocot_path(n, d): + for _ in range(count): + mid = (left[0] + right[0], left[1] + right[1]) + if direction == "L": + right = mid + else: + left = mid + self.assertEqual(Fraction(left[0] + right[0], left[1] + right[1]), Fraction(n, d)) + self.assertEqual(stern_brocot_path(10**100, 1), [("R", 10**100 - 1)]) + + def test_invalid(self): + for args in [(0, 1), (1, -1), (True, 1)]: + with self.assertRaises(ValueError): + stern_brocot_path(*args) diff --git a/tests/test_stirling_second.py b/tests/test_stirling_second.py new file mode 100644 index 0000000..14dfb15 --- /dev/null +++ b/tests/test_stirling_second.py @@ -0,0 +1,20 @@ +import itertools +import math +import unittest + +from algorithm_lab.stirling_second import stirling_second + + +class Tests(unittest.TestCase): + def test_surjection_oracle(self): + for n in range(7): + for k in range(5): + onto = sum( + len(set(values)) == k for values in itertools.product(range(k), repeat=n) + ) + self.assertEqual(stirling_second(n, k), onto // math.factorial(k)) + + def test_invalid(self): + for args in [(-1, 1), (1, -1), (True, 1), (1, 1.5)]: + with self.assertRaises(ValueError): + stirling_second(*args) diff --git a/tests/test_subset_mobius_transform.py b/tests/test_subset_mobius_transform.py new file mode 100644 index 0000000..5ac6cab --- /dev/null +++ b/tests/test_subset_mobius_transform.py @@ -0,0 +1,25 @@ +import random +import unittest + +from algorithm_lab.subset_mobius_transform import subset_mobius_transform + + +class Tests(unittest.TestCase): + def test_inclusion_exclusion_oracle(self): + rng = random.Random(228) + for bits in range(7): + values = [rng.randrange(-10, 11) for _ in range(1 << bits)] + expected = [ + sum( + (-1) ** (mask.bit_count() - sub.bit_count()) * value + for sub, value in enumerate(values) + if sub & mask == sub + ) + for mask in range(len(values)) + ] + self.assertEqual(subset_mobius_transform(values), expected) + + def test_invalid(self): + for values in [[], [1, 2, 3], [True], [1.5]]: + with self.assertRaises(ValueError): + subset_mobius_transform(values) diff --git a/tests/test_subset_zeta_transform.py b/tests/test_subset_zeta_transform.py new file mode 100644 index 0000000..ce009ec --- /dev/null +++ b/tests/test_subset_zeta_transform.py @@ -0,0 +1,21 @@ +import random +import unittest + +from algorithm_lab.subset_zeta_transform import subset_zeta_transform + + +class Tests(unittest.TestCase): + def test_subset_enumeration(self): + rng = random.Random(227) + for bits in range(7): + values = [rng.randrange(-10, 11) for _ in range(1 << bits)] + expected = [ + sum(value for sub, value in enumerate(values) if sub & mask == sub) + for mask in range(len(values)) + ] + self.assertEqual(subset_zeta_transform(values), expected) + + def test_invalid(self): + for values in [[], [1, 2, 3], [True], [1.5]]: + with self.assertRaises(ValueError): + subset_zeta_transform(values)