Skip to content

feat: add solovay strassen primality test #5692

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.thealgorithms.maths;

import java.util.Random;

/**
* This class implements the Solovay-Strassen primality test,
* which is a probabilistic algorithm to determine whether a number is prime.
* The algorithm is based on properties of the Jacobi symbol and modular exponentiation.
*
* For more information, go to {@link https://en.wikipedia.org/wiki/Solovay%E2%80%93Strassen_primality_test}
*/
final class SolovayStrassenPrimalityTest {

private final Random random;

/**
* Constructs a SolovayStrassenPrimalityTest instance with a specified seed for randomness.
*
* @param seed the seed for generating random numbers
*/
private SolovayStrassenPrimalityTest(int seed) {
random = new Random(seed);
}

/**
* Factory method to create an instance of SolovayStrassenPrimalityTest.
*
* @param seed the seed for generating random numbers
* @return a new instance of SolovayStrassenPrimalityTest
*/
public static SolovayStrassenPrimalityTest getSolovayStrassenPrimalityTest(int seed) {
return new SolovayStrassenPrimalityTest(seed);
}

/**
* Calculates modular exponentiation using the method of exponentiation by squaring.
*
* @param base the base number
* @param exponent the exponent
* @param mod the modulus
* @return (base^exponent) mod mod
*/
private static long calculateModularExponentiation(long base, long exponent, long mod) {
long x = 1; // This will hold the result of (base^exponent) % mod
long y = base; // This holds the current base value being squared

while (exponent > 0) {
// If exponent is odd, multiply the current base (y) with x
if (exponent % 2 == 1) {
x = x * y % mod; // Update result with current base
}
// Square the base for the next iteration
y = y * y % mod; // Update base to be y^2
exponent = exponent / 2; // Halve the exponent for next iteration
}

return x % mod; // Return final result after all iterations
}

/**
* Computes the Jacobi symbol (a/n), which is a generalization of the Legendre symbol.
*
* @param a the numerator
* @param num the denominator (must be an odd positive integer)
* @return the Jacobi symbol value: 1, -1, or 0
*/
public int calculateJacobi(long a, long num) {
// Check if num is non-positive or even; Jacobi symbol is not defined in these cases
if (num <= 0 || num % 2 == 0) {
return 0;
}

a = a % num; // Reduce a modulo num to simplify calculations
int jacobi = 1; // Initialize Jacobi symbol value

while (a != 0) {
// While a is even, reduce it and adjust jacobi based on properties of num
while (a % 2 == 0) {
a /= 2; // Divide a by 2 until it becomes odd
long nMod8 = num % 8; // Get num modulo 8 to check conditions for jacobi adjustment
if (nMod8 == 3 || nMod8 == 5) {
jacobi = -jacobi; // Flip jacobi sign based on properties of num modulo 8
}
}

long temp = a; // Temporarily store value of a
a = num; // Set a to be num for next iteration
num = temp; // Set num to be previous value of a

// Adjust jacobi based on properties of both numbers when both are odd and congruent to 3 modulo 4
if (a % 4 == 3 && num % 4 == 3) {
jacobi = -jacobi; // Flip jacobi sign again based on congruences
}

a = a % num; // Reduce a modulo num for next iteration of Jacobi computation
}

return (num == 1) ? jacobi : 0; // If num reduces to 1, return jacobi value, otherwise return 0 (not defined)
}

/**
* Performs the Solovay-Strassen primality test on a given number.
*
* @param num the number to be tested for primality
* @param iterations the number of iterations to run for accuracy
* @return true if num is likely prime, false if it is composite
*/
public boolean solovayStrassen(long num, int iterations) {
if (num <= 1) {
return false; // Numbers <=1 are not prime by definition.
}
if (num <= 3) {
return true; // Numbers <=3 are prime.
}

for (int i = 0; i < iterations; i++) {
long r = Math.abs(random.nextLong() % (num - 1)) + 2; // Generate a non-negative random number.
long a = r % (num - 1) + 1; // Choose random 'a' in range [1, n-1].

long jacobi = (num + calculateJacobi(a, num)) % num;
// Calculate Jacobi symbol and adjust it modulo n.

long mod = calculateModularExponentiation(a, (num - 1) / 2, num);
// Calculate modular exponentiation: a^((n-1)/2) mod n.

if (jacobi == 0 || mod != jacobi) {
return false; // If Jacobi symbol is zero or doesn't match modular result, n is composite.
}
}

return true; // If no contradictions found after all iterations, n is likely prime.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.thealgorithms.maths;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

/**
* Unit tests for the {@link SolovayStrassenPrimalityTest} class.
* This class tests the functionality of the Solovay-Strassen primality test implementation.
*/
class SolovayStrassenPrimalityTestTest {

private static final int RANDOM_SEED = 123; // Seed for reproducibility
private SolovayStrassenPrimalityTest testInstance;

/**
* Sets up a new instance of {@link SolovayStrassenPrimalityTest}
* before each test case, using a fixed random seed for consistency.
*/
@BeforeEach
void setUp() {
testInstance = SolovayStrassenPrimalityTest.getSolovayStrassenPrimalityTest(RANDOM_SEED);
}

/**
* Provides test cases for prime numbers with various values of n and k (iterations).
*
* @return an array of objects containing pairs of n and k values
*/
static Object[][] primeNumbers() {
return new Object[][] {{2, 1}, {3, 1}, {5, 5}, {7, 10}, {11, 20}, {13, 10}, {17, 5}, {19, 1}};
}

/**
* Tests known prime numbers with various values of n and k (iterations).
*
* @param n the number to be tested for primality
* @param k the number of iterations to use in the primality test
*/
@ParameterizedTest
@MethodSource("primeNumbers")
void testPrimeNumbersWithDifferentNAndK(int n, int k) {
assertTrue(testInstance.solovayStrassen(n, k), n + " should be prime");
}

/**
* Provides test cases for composite numbers with various values of n and k (iterations).
*
* @return an array of objects containing pairs of n and k values
*/
static Object[][] compositeNumbers() {
return new Object[][] {{4, 1}, {6, 5}, {8, 10}, {9, 20}, {10, 1}, {12, 5}, {15, 10}};
}

/**
* Tests known composite numbers with various values of n and k (iterations).
*
* @param n the number to be tested for primality
* @param k the number of iterations to use in the primality test
*/
@ParameterizedTest
@MethodSource("compositeNumbers")
void testCompositeNumbersWithDifferentNAndK(int n, int k) {
assertFalse(testInstance.solovayStrassen(n, k), n + " should be composite");
}

/**
* Tests edge cases for the primality test.
* This includes negative numbers and small integers (0 and 1).
*/
@Test
void testEdgeCases() {
assertFalse(testInstance.solovayStrassen(-1, 10), "-1 should not be prime");
assertFalse(testInstance.solovayStrassen(0, 10), "0 should not be prime");
assertFalse(testInstance.solovayStrassen(1, 10), "1 should not be prime");

// Test small primes and composites
assertTrue(testInstance.solovayStrassen(2, 1), "2 is a prime number (single iteration)");
assertFalse(testInstance.solovayStrassen(9, 1), "9 is a composite number (single iteration)");

// Test larger primes and composites
long largePrime = 104729; // Known large prime number
long largeComposite = 104730; // Composite number (even)

assertTrue(testInstance.solovayStrassen(largePrime, 20), "104729 is a prime number");
assertFalse(testInstance.solovayStrassen(largeComposite, 20), "104730 is a composite number");

// Test very large numbers (may take longer)
long veryLargePrime = 512927357; // Known very large prime number
long veryLargeComposite = 512927358; // Composite number (even)

assertTrue(testInstance.solovayStrassen(veryLargePrime, 20), Long.MAX_VALUE - 1 + " is likely a prime number.");

assertFalse(testInstance.solovayStrassen(veryLargeComposite, 20), Long.MAX_VALUE + " is a composite number.");
}

/**
* Tests the Jacobi symbol calculation directly for known values.
* This verifies that the Jacobi symbol method behaves as expected.
*/
@Test
void testJacobiSymbolCalculation() {
// Jacobi symbol (a/n) where n is odd and positive
int jacobi1 = testInstance.calculateJacobi(6, 11); // Should return -1
int jacobi2 = testInstance.calculateJacobi(5, 11); // Should return +1

assertEquals(-1, jacobi1);
assertEquals(+1, jacobi2);

// Edge case: Jacobi symbol with even n or non-positive n
int jacobi4 = testInstance.calculateJacobi(5, -11); // Should return 0 (invalid)
int jacobi5 = testInstance.calculateJacobi(5, 0); // Should return 0 (invalid)

assertEquals(0, jacobi4);
assertEquals(0, jacobi5);
}
}