diff --git a/src/main/java/com/williamfiset/algorithms/math/PrimeFactorization.java b/src/main/java/com/williamfiset/algorithms/math/PrimeFactorization.java index 85c497e08..c2cbfab61 100644 --- a/src/main/java/com/williamfiset/algorithms/math/PrimeFactorization.java +++ b/src/main/java/com/williamfiset/algorithms/math/PrimeFactorization.java @@ -1,66 +1,136 @@ +/** + * Prime factorization using Pollard's rho algorithm with Miller-Rabin primality testing. + * + *

Miller-Rabin with the deterministic witness set {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37} + * is guaranteed correct for all n < 3.317 × 10^24, which covers the full range of Java's long. + * + *

Time: O(n^(1/4)) expected per factor found + * + * @author William Fiset, william.alexandre.fiset@gmail.com + */ package com.williamfiset.algorithms.math; import java.util.ArrayList; -import java.util.PriorityQueue; +import java.util.List; public class PrimeFactorization { - public static ArrayList primeFactorization(long n) { - ArrayList factors = new ArrayList<>(); - if (n <= 0) throw new IllegalArgumentException(); - else if (n == 1) return factors; - PriorityQueue divisorQueue = new PriorityQueue<>(); - divisorQueue.add(n); - while (!divisorQueue.isEmpty()) { - long divisor = divisorQueue.remove(); - if (isPrime(divisor)) { - factors.add(divisor); - continue; - } - long next_divisor = pollardRho(divisor); - if (next_divisor == divisor) { - divisorQueue.add(divisor); - } else { - divisorQueue.add(next_divisor); - divisorQueue.add(divisor / next_divisor); - } - } + /** + * Returns the prime factorization of n. The returned factors are not necessarily sorted. + * + * @throws IllegalArgumentException if n <= 0. + */ + public static List primeFactorization(long n) { + if (n <= 0) + throw new IllegalArgumentException(); + + List factors = new ArrayList<>(); + factor(n, factors); return factors; } + private static void factor(long n, List factors) { + if (n == 1) + return; + if (isPrime(n)) { + factors.add(n); + return; + } + long d = pollardRho(n); + factor(d, factors); + factor(n / d, factors); + } + private static long pollardRho(long n) { - if (n % 2 == 0) return 2; + if (n % 2 == 0) + return 2; long x = 2 + (long) (999999 * Math.random()); long c = 2 + (long) (999999 * Math.random()); long y = x; long d = 1; while (d == 1) { - x = (x * x + c) % n; - y = (y * y + c) % n; - y = (y * y + c) % n; + x = mulMod(x, x, n) + c; + if (x >= n) + x -= n; + y = mulMod(y, y, n) + c; + if (y >= n) + y -= n; + y = mulMod(y, y, n) + c; + if (y >= n) + y -= n; d = gcd(Math.abs(x - y), n); - if (d == n) break; + if (d == n) + break; } return d; } - private static long gcd(long a, long b) { - return b == 0 ? a : gcd(b, a % b); - } + /** + * Deterministic Miller-Rabin primality test, correct for all long values. Uses 12 witnesses that + * guarantee correctness for n < 3.317 × 10^24. + */ + private static boolean isPrime(long n) { + if (n < 2) + return false; + if (n < 4) + return true; + if (n % 2 == 0 || n % 3 == 0) + return false; + + // Write n-1 as 2^r * d + long d = n - 1; + int r = Long.numberOfTrailingZeros(d); + d >>= r; - private static boolean isPrime(final long n) { - if (n < 2) return false; - if (n == 2 || n == 3) return true; - if (n % 2 == 0 || n % 3 == 0) return false; - long limit = (long) Math.sqrt(n); - for (long i = 5; i <= limit; i += 6) if (n % i == 0 || n % (i + 2) == 0) return false; + for (long a : new long[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37}) { + if (a >= n) + continue; + long x = powMod(a, d, n); + if (x == 1 || x == n - 1) + continue; + boolean composite = true; + for (int i = 0; i < r - 1; i++) { + x = mulMod(x, x, n); + if (x == n - 1) { + composite = false; + break; + } + } + if (composite) + return false; + } return true; } + /** Modular exponentiation: (base^exp) % mod, using mulMod to avoid overflow. */ + private static long powMod(long base, long exp, long mod) { + long result = 1; + base %= mod; + while (exp > 0) { + if ((exp & 1) == 1) + result = mulMod(result, base, mod); + exp >>= 1; + base = mulMod(base, base, mod); + } + return result; + } + + /** Overflow-safe modular multiplication: (a * b) % mod. */ + private static long mulMod(long a, long b, long mod) { + return java.math.BigInteger.valueOf(a) + .multiply(java.math.BigInteger.valueOf(b)) + .mod(java.math.BigInteger.valueOf(mod)) + .longValue(); + } + + private static long gcd(long a, long b) { + return b == 0 ? a : gcd(b, a % b); + } + public static void main(String[] args) { - System.out.println(primeFactorization(7)); // [7] - System.out.println(primeFactorization(100)); // [2,2,5,5] - System.out.println(primeFactorization(666)); // [2,3,3,37] - System.out.println(primeFactorization(872342345)); // [5, 7, 7, 67, 19, 2797] + System.out.println(primeFactorization(7)); // [7] + System.out.println(primeFactorization(100)); // [2, 2, 5, 5] + System.out.println(primeFactorization(666)); // [2, 3, 3, 37] + System.out.println(primeFactorization(872342345)); // [5, 7, 7, 19, 67, 2797] } } diff --git a/src/test/java/com/williamfiset/algorithms/math/BUILD b/src/test/java/com/williamfiset/algorithms/math/BUILD index 12257dace..138ee1134 100644 --- a/src/test/java/com/williamfiset/algorithms/math/BUILD +++ b/src/test/java/com/williamfiset/algorithms/math/BUILD @@ -26,6 +26,17 @@ java_test( deps = TEST_DEPS, ) +# bazel test //src/test/java/com/williamfiset/algorithms/math:PrimeFactorizationTest +java_test( + name = "PrimeFactorizationTest", + srcs = ["PrimeFactorizationTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.math.PrimeFactorizationTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + # bazel test //src/test/java/com/williamfiset/algorithms/math:EulerTotientFunctionTest java_test( name = "EulerTotientFunctionTest", diff --git a/src/test/java/com/williamfiset/algorithms/math/PrimeFactorizationTest.java b/src/test/java/com/williamfiset/algorithms/math/PrimeFactorizationTest.java new file mode 100644 index 000000000..3db51c542 --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/math/PrimeFactorizationTest.java @@ -0,0 +1,90 @@ +package com.williamfiset.algorithms.math; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.*; +import org.junit.jupiter.api.*; + +public class PrimeFactorizationTest { + + private static void assertFactors(long n, Long... expected) { + List factors = PrimeFactorization.primeFactorization(n); + Collections.sort(factors); + assertThat(factors).containsExactlyElementsIn(Arrays.asList(expected)).inOrder(); + } + + @Test + public void testOne() { + assertFactors(1); + } + + @Test + public void testSmallPrimes() { + assertFactors(2, 2L); + assertFactors(3, 3L); + assertFactors(5, 5L); + assertFactors(7, 7L); + assertFactors(11, 11L); + assertFactors(13, 13L); + } + + @Test + public void testPowersOfTwo() { + assertFactors(4, 2L, 2L); + assertFactors(8, 2L, 2L, 2L); + assertFactors(1024, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L); + } + + @Test + public void testPrimePowers() { + assertFactors(27, 3L, 3L, 3L); + assertFactors(125, 5L, 5L, 5L); + assertFactors(49, 7L, 7L); + } + + @Test + public void testComposites() { + assertFactors(100, 2L, 2L, 5L, 5L); + assertFactors(666, 2L, 3L, 3L, 37L); + assertFactors(872342345, 5L, 7L, 7L, 19L, 67L, 2797L); + } + + @Test + public void testProductEqualsOriginal() { + long[] values = {12, 360, 872342345, 1000000007, 239821585064027L}; + for (long n : values) { + List factors = PrimeFactorization.primeFactorization(n); + long product = 1; + for (long f : factors) + product *= f; + assertThat(product).isEqualTo(n); + } + } + + @Test + public void testLargePrime() { + assertFactors(8763857775536878331L, 8763857775536878331L); + } + + @Test + public void testLargeSemiprime() { + // 15485867 and 15486481 are both prime + assertFactors(239821585064027L, 15485867L, 15486481L); + } + + @Test + public void testLargeComposite() { + assertFactors(1000000007L * 999999937L, 999999937L, 1000000007L); + } + + @Test + public void testZeroThrows() { + assertThrows(IllegalArgumentException.class, () -> PrimeFactorization.primeFactorization(0)); + } + + @Test + public void testNegativeThrows() { + assertThrows(IllegalArgumentException.class, () -> PrimeFactorization.primeFactorization(-5)); + } +}