Skip to content

Commit 79544c8

Browse files
saahil-mahatoalxkm
andauthored
feat: add solovay strassen primality test (#5692)
* feat: add solovay strassen primality test * chore: add wikipedia link * fix: format and coverage * fix: mvn stylecheck * fix: PMD errors * refactor: make random final --------- Co-authored-by: Alex Klymenko <[email protected]>
1 parent b1aeac5 commit 79544c8

File tree

2 files changed

+255
-0
lines changed

2 files changed

+255
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.thealgorithms.maths;
2+
3+
import java.util.Random;
4+
5+
/**
6+
* This class implements the Solovay-Strassen primality test,
7+
* which is a probabilistic algorithm to determine whether a number is prime.
8+
* The algorithm is based on properties of the Jacobi symbol and modular exponentiation.
9+
*
10+
* For more information, go to {@link https://en.wikipedia.org/wiki/Solovay%E2%80%93Strassen_primality_test}
11+
*/
12+
final class SolovayStrassenPrimalityTest {
13+
14+
private final Random random;
15+
16+
/**
17+
* Constructs a SolovayStrassenPrimalityTest instance with a specified seed for randomness.
18+
*
19+
* @param seed the seed for generating random numbers
20+
*/
21+
private SolovayStrassenPrimalityTest(int seed) {
22+
random = new Random(seed);
23+
}
24+
25+
/**
26+
* Factory method to create an instance of SolovayStrassenPrimalityTest.
27+
*
28+
* @param seed the seed for generating random numbers
29+
* @return a new instance of SolovayStrassenPrimalityTest
30+
*/
31+
public static SolovayStrassenPrimalityTest getSolovayStrassenPrimalityTest(int seed) {
32+
return new SolovayStrassenPrimalityTest(seed);
33+
}
34+
35+
/**
36+
* Calculates modular exponentiation using the method of exponentiation by squaring.
37+
*
38+
* @param base the base number
39+
* @param exponent the exponent
40+
* @param mod the modulus
41+
* @return (base^exponent) mod mod
42+
*/
43+
private static long calculateModularExponentiation(long base, long exponent, long mod) {
44+
long x = 1; // This will hold the result of (base^exponent) % mod
45+
long y = base; // This holds the current base value being squared
46+
47+
while (exponent > 0) {
48+
// If exponent is odd, multiply the current base (y) with x
49+
if (exponent % 2 == 1) {
50+
x = x * y % mod; // Update result with current base
51+
}
52+
// Square the base for the next iteration
53+
y = y * y % mod; // Update base to be y^2
54+
exponent = exponent / 2; // Halve the exponent for next iteration
55+
}
56+
57+
return x % mod; // Return final result after all iterations
58+
}
59+
60+
/**
61+
* Computes the Jacobi symbol (a/n), which is a generalization of the Legendre symbol.
62+
*
63+
* @param a the numerator
64+
* @param num the denominator (must be an odd positive integer)
65+
* @return the Jacobi symbol value: 1, -1, or 0
66+
*/
67+
public int calculateJacobi(long a, long num) {
68+
// Check if num is non-positive or even; Jacobi symbol is not defined in these cases
69+
if (num <= 0 || num % 2 == 0) {
70+
return 0;
71+
}
72+
73+
a = a % num; // Reduce a modulo num to simplify calculations
74+
int jacobi = 1; // Initialize Jacobi symbol value
75+
76+
while (a != 0) {
77+
// While a is even, reduce it and adjust jacobi based on properties of num
78+
while (a % 2 == 0) {
79+
a /= 2; // Divide a by 2 until it becomes odd
80+
long nMod8 = num % 8; // Get num modulo 8 to check conditions for jacobi adjustment
81+
if (nMod8 == 3 || nMod8 == 5) {
82+
jacobi = -jacobi; // Flip jacobi sign based on properties of num modulo 8
83+
}
84+
}
85+
86+
long temp = a; // Temporarily store value of a
87+
a = num; // Set a to be num for next iteration
88+
num = temp; // Set num to be previous value of a
89+
90+
// Adjust jacobi based on properties of both numbers when both are odd and congruent to 3 modulo 4
91+
if (a % 4 == 3 && num % 4 == 3) {
92+
jacobi = -jacobi; // Flip jacobi sign again based on congruences
93+
}
94+
95+
a = a % num; // Reduce a modulo num for next iteration of Jacobi computation
96+
}
97+
98+
return (num == 1) ? jacobi : 0; // If num reduces to 1, return jacobi value, otherwise return 0 (not defined)
99+
}
100+
101+
/**
102+
* Performs the Solovay-Strassen primality test on a given number.
103+
*
104+
* @param num the number to be tested for primality
105+
* @param iterations the number of iterations to run for accuracy
106+
* @return true if num is likely prime, false if it is composite
107+
*/
108+
public boolean solovayStrassen(long num, int iterations) {
109+
if (num <= 1) {
110+
return false; // Numbers <=1 are not prime by definition.
111+
}
112+
if (num <= 3) {
113+
return true; // Numbers <=3 are prime.
114+
}
115+
116+
for (int i = 0; i < iterations; i++) {
117+
long r = Math.abs(random.nextLong() % (num - 1)) + 2; // Generate a non-negative random number.
118+
long a = r % (num - 1) + 1; // Choose random 'a' in range [1, n-1].
119+
120+
long jacobi = (num + calculateJacobi(a, num)) % num;
121+
// Calculate Jacobi symbol and adjust it modulo n.
122+
123+
long mod = calculateModularExponentiation(a, (num - 1) / 2, num);
124+
// Calculate modular exponentiation: a^((n-1)/2) mod n.
125+
126+
if (jacobi == 0 || mod != jacobi) {
127+
return false; // If Jacobi symbol is zero or doesn't match modular result, n is composite.
128+
}
129+
}
130+
131+
return true; // If no contradictions found after all iterations, n is likely prime.
132+
}
133+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package com.thealgorithms.maths;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.params.ParameterizedTest;
10+
import org.junit.jupiter.params.provider.MethodSource;
11+
12+
/**
13+
* Unit tests for the {@link SolovayStrassenPrimalityTest} class.
14+
* This class tests the functionality of the Solovay-Strassen primality test implementation.
15+
*/
16+
class SolovayStrassenPrimalityTestTest {
17+
18+
private static final int RANDOM_SEED = 123; // Seed for reproducibility
19+
private SolovayStrassenPrimalityTest testInstance;
20+
21+
/**
22+
* Sets up a new instance of {@link SolovayStrassenPrimalityTest}
23+
* before each test case, using a fixed random seed for consistency.
24+
*/
25+
@BeforeEach
26+
void setUp() {
27+
testInstance = SolovayStrassenPrimalityTest.getSolovayStrassenPrimalityTest(RANDOM_SEED);
28+
}
29+
30+
/**
31+
* Provides test cases for prime numbers with various values of n and k (iterations).
32+
*
33+
* @return an array of objects containing pairs of n and k values
34+
*/
35+
static Object[][] primeNumbers() {
36+
return new Object[][] {{2, 1}, {3, 1}, {5, 5}, {7, 10}, {11, 20}, {13, 10}, {17, 5}, {19, 1}};
37+
}
38+
39+
/**
40+
* Tests known prime numbers with various values of n and k (iterations).
41+
*
42+
* @param n the number to be tested for primality
43+
* @param k the number of iterations to use in the primality test
44+
*/
45+
@ParameterizedTest
46+
@MethodSource("primeNumbers")
47+
void testPrimeNumbersWithDifferentNAndK(int n, int k) {
48+
assertTrue(testInstance.solovayStrassen(n, k), n + " should be prime");
49+
}
50+
51+
/**
52+
* Provides test cases for composite numbers with various values of n and k (iterations).
53+
*
54+
* @return an array of objects containing pairs of n and k values
55+
*/
56+
static Object[][] compositeNumbers() {
57+
return new Object[][] {{4, 1}, {6, 5}, {8, 10}, {9, 20}, {10, 1}, {12, 5}, {15, 10}};
58+
}
59+
60+
/**
61+
* Tests known composite numbers with various values of n and k (iterations).
62+
*
63+
* @param n the number to be tested for primality
64+
* @param k the number of iterations to use in the primality test
65+
*/
66+
@ParameterizedTest
67+
@MethodSource("compositeNumbers")
68+
void testCompositeNumbersWithDifferentNAndK(int n, int k) {
69+
assertFalse(testInstance.solovayStrassen(n, k), n + " should be composite");
70+
}
71+
72+
/**
73+
* Tests edge cases for the primality test.
74+
* This includes negative numbers and small integers (0 and 1).
75+
*/
76+
@Test
77+
void testEdgeCases() {
78+
assertFalse(testInstance.solovayStrassen(-1, 10), "-1 should not be prime");
79+
assertFalse(testInstance.solovayStrassen(0, 10), "0 should not be prime");
80+
assertFalse(testInstance.solovayStrassen(1, 10), "1 should not be prime");
81+
82+
// Test small primes and composites
83+
assertTrue(testInstance.solovayStrassen(2, 1), "2 is a prime number (single iteration)");
84+
assertFalse(testInstance.solovayStrassen(9, 1), "9 is a composite number (single iteration)");
85+
86+
// Test larger primes and composites
87+
long largePrime = 104729; // Known large prime number
88+
long largeComposite = 104730; // Composite number (even)
89+
90+
assertTrue(testInstance.solovayStrassen(largePrime, 20), "104729 is a prime number");
91+
assertFalse(testInstance.solovayStrassen(largeComposite, 20), "104730 is a composite number");
92+
93+
// Test very large numbers (may take longer)
94+
long veryLargePrime = 512927357; // Known very large prime number
95+
long veryLargeComposite = 512927358; // Composite number (even)
96+
97+
assertTrue(testInstance.solovayStrassen(veryLargePrime, 20), Long.MAX_VALUE - 1 + " is likely a prime number.");
98+
99+
assertFalse(testInstance.solovayStrassen(veryLargeComposite, 20), Long.MAX_VALUE + " is a composite number.");
100+
}
101+
102+
/**
103+
* Tests the Jacobi symbol calculation directly for known values.
104+
* This verifies that the Jacobi symbol method behaves as expected.
105+
*/
106+
@Test
107+
void testJacobiSymbolCalculation() {
108+
// Jacobi symbol (a/n) where n is odd and positive
109+
int jacobi1 = testInstance.calculateJacobi(6, 11); // Should return -1
110+
int jacobi2 = testInstance.calculateJacobi(5, 11); // Should return +1
111+
112+
assertEquals(-1, jacobi1);
113+
assertEquals(+1, jacobi2);
114+
115+
// Edge case: Jacobi symbol with even n or non-positive n
116+
int jacobi4 = testInstance.calculateJacobi(5, -11); // Should return 0 (invalid)
117+
int jacobi5 = testInstance.calculateJacobi(5, 0); // Should return 0 (invalid)
118+
119+
assertEquals(0, jacobi4);
120+
assertEquals(0, jacobi5);
121+
}
122+
}

0 commit comments

Comments
 (0)