Introduction
Given a positive integer , try to quickly find one of its non-trivial factors.
Consider the naive algorithm. Factors occur in pairs, so all factors of can be divided into and . It is enough to enumerate the numbers in and use division to find at least two factors. This method has time complexity .
When , the running time of this algorithm is unacceptable, so we seek a better algorithm. One idea is to guess randomly whether a number is a factor of . A lucky guess can find the answer in time, but for the probability of a successful guess is , and the expected number of guesses is . Guessing within improves the success probability somewhat. We want to optimize this guessing process.
Naive algorithm
The simplest algorithm enumerates .
C++
vector<int> breakdown(int N) {
vector<int> result;
for (int i = 2; i * i <= N; i++) {
if (N % i == 0) { // If i divides N, then i is a prime factor of N.
while (N % i == 0) N /= i;
result.push_back(i);
}
}
if (N != 1) { // The operations have left a prime factor in N
result.push_back(N);
}
return result;
}
Python
def breakdown(N):
result = []
for i in range(2, int(sqrt(N)) + 1):
if N % i == 0: # If i divides N, then i is a prime factor of N.
while N % i == 0:
N //= i
result.append(i)
if N != 1: # The operations have left a prime factor in N
result.append(N)
return result
We can prove that every element in result is a prime factor of N.
Proof that N i while(N % i == 0) N /= i i N N i N i result i result i N % i == 0 i N N i i N result result result i i K i result N i i * i > N i i N result result i i i i N i N N N result contains all prime factors of
First consider how changes.When the loop finishes processing , the completed step ensures that no longer divides .Each factor removal also preserves divisibility of the original by the current value.Thus, when processing of begins, the current is a factor of the original and is not divisible by any integer smaller than .
Next, every element in is a factor of .The condition for inserting into is , so divides the current , which is a factor of the original .When the loop ends, a remaining other than one is likewise a factor of the original number.
We next prove that all elements of are prime.Suppose a composite occurs in contains a composite , so some factor is no greater than .Such a cannot be inserted during the loop: by the first part, when the loop reaches , the current is not divisible by any smaller .Nor can it be inserted after the loop, because the exit condition means every has already been checked, and none divides the current .
Finally, every prime factor of must occur in .Suppose that is a prime factor of but is absent from .By the discussion above, cannot be an encountered in the loop.Let be the last value before the loop exits. Then , and the remaining is not divisible by any earlier , while still divides it.Hence the remaining is greater than one and, by the preceding argument, is prime. Thus it equals and is inserted into , a contradiction.
It is worth noting that if a prime table has already been prepared, the time complexity decreases from to . See sieves for more information about precomputing such tables.
Example: CF 1445C
Pollard-Rho algorithm
Introduction
The brute-force complexity of finding a non-trivial factor is , where is the smallest prime factor of . The Pollard-Rho algorithm introduced below is randomized and finds a non-trivial factor in expected time (Note: a non-trivial factor need not be prime).
The key idea is to take a random self-map , start from any point , and iterate . The sequence enters a cycle in expected time . If we find , then divides , and this gcd is a non-trivial factor of .
The birthday paradox provides intuition for the expected time to enter a cycle.
Birthday paradox
Ignoring birth years (and assuming 365 days in every year), how many people must a room contain for the probability that two of them share a birthday to reach ?
Solution: suppose a year has days and the room has people, numbered . Assume each birthday is uniformly distributed among the days and that birthdays are mutually independent.
Let event be that the people have distinct birthdays. Its probability is
The probability that at least two people share a birthday is . The problem requires , so
Using the inequality , we obtain
Therefore
Substituting gives . Thus a room needs at least people for the probability of a shared birthday to reach . This mathematical fact is highly counterintuitive, so it is called a paradox.
When and , the probability of a shared birthday exceeds 1. More generally, in a year with days, when a room contains people, the probability that at least two share a birthday is about .
Similarly, if birthdays are sampled independently and uniformly, the expected number of samples needed to obtain the first repeated birthday is also . Let this number be . Then
This suggests that if we can sample a sequence of numbers at random, the expected sample size before a repeated number appears is also .
Finding a factor using the greatest common divisor
Constructing a random sequence modulo is impractical because is precisely what we seek. Instead, generate a pseudorandom sequence using : choose randomly and set , where is a randomly chosen constant.
The chosen function is easy to compute and often generates a sufficiently random-looking sequence, but it is not truly random. For example, let , , and ; the values generated by are
After , the values cycle among .Arranging these values as in the figure produces a shape resembling , which gives the algorithm its name.
More importantly, this function really is a self-map on : if , then .
Proof
If , then . Notice that , where is an integer depending on , and . Thus , and consequently .
As a sequence obtained by repeatedly iterating a pseudorandom self-map on , repeats within expected time . Once we observe , we can obtain a non-trivial factor of from . Since is unknown, we cannot detect the repetition directly; a simple test is that is strictly greater than one.
The algorithm does not always succeed, because may equal , meaning . In that case, when first repeats, also repeats. No non-trivial factor is obtained. Once enters a cycle, further iteration is pointless because the same cycle will repeat. The algorithm should report factorization failure and retry with a different choice of in .
According to the analysis above, any function satisfying and providing sufficient pseudorandomness (for example, certain polynomial functions) can be used here. In practice, is used most often.2
Implementation
The implementation must quickly determine during iteration whether has repeated. Viewing as edges of a directed graph whose vertices are , we need a cycle-detection algorithm, replacing equality testing with the test whether is greater than one.
Floyd cycle detection
Suppose two people race around a track, with A faster than B. After enough time A must meet B, and at the meeting point the distance run by A minus the distance run by B is a multiple of the track length.
Let and . At each step update and . If and become equal during the updates, a cycle has been found.
At each step set and test whether ; if so, return . If , then has formed a cycle, so stop and return itself, adjust the random constant , and try factorization again.
Pollard-Rho algorithm based on Floyd cycle detection
C++
ll Pollard_Rho(ll N) {
if (N == 4) return 2; // We initially advance by two steps, so 4 needs a special case
ll c = rand() % (N - 1) + 1;
ll t = f(0, c, N);
ll r = f(f(0, c, N), c, N);
while (t != r) {
ll d = gcd(abs(t - r), N);
if (d > 1) return d;
t = f(t, c, N);
r = f(f(r, c, N), c, N);
}
return N;
}
Python
import random
def Pollard_Rho(N):
if N == 4:
return 2 # We initially advance by two steps, so 4 needs a special case
c = random.randint(1, N - 1)
t = f(0, c, N)
r = f(f(0, c, N), c, N)
while t != r:
d = gcd(abs(t - r), N)
if d > 1:
return d
t = f(t, c, N)
r = f(f(r, c, N), c, N)
return N
Brent cycle detection
The Floyd cycle-detection algorithm admits a constant-factor improvement. Brent's algorithm increases from . In round , A stays still while B moves forward steps. If B meets A, the cycle has been found; otherwise A jumps to B's position and the next round begins.
It can be proved3 that the number of calls to before finding a cycle is never greater than for Floyd detection. Tests in the original paper show that Brent detection reduces the average time by compared with Floyd detection.
Doubling optimization
Both Floyd and Brent cycle detection use iterations. However, computing a at every iteration slows the algorithm. Multiplicative accumulation can reduce the number of gcd computations.
In short, if , then for every . Thus if , some pair necessarily satisfies . If the product becomes zero, factorization has failed; stop and return itself.
If a gcd is computed once every pairs, the complexity falls to , where is the cost of one gcd computation. When and have roughly the same order, the expected complexity is . Implementations usually choose .
Here is an implementation of Pollard-Rho using Brent cycle detection and the doubling optimization.
Implementation
C++
ll Pollard_Rho(ll x) {
ll t = 0;
ll c = rand() % (x - 1) + 1;
ll s = t;
int step = 0, goal = 1;
ll val = 1;
for (goal = 1;; goal <<= 1, s = t, val = 1) {
for (step = 1; step <= goal; ++step) {
t = f(t, c, x);
val = val * abs(t - s) % x;
// If val is zero, stop and factor again
if (!val) return x;
if (step % 127 == 0) {
ll d = gcd(val, x);
if (d > 1) return d;
}
}
ll d = gcd(val, x);
if (d > 1) return d;
}
}
Python
from random import randint
from math import gcd
def Pollard_Rho(x):
c = randint(1, x - 1)
s = t = f(0, c, x)
goal = val = 1
while True:
for step in range(1, goal + 1):
t = f(t, c, x)
val = val * abs(t - s) % x
if val == 0:
return x # If val is zero, stop and factor again
if step % 127 == 0:
d = gcd(val, x)
if d > 1:
return d
d = gcd(val, x)
if d > 1:
return d
s = t
goal <<= 1
val = 1
Complexity
The expected number of iterations in Pollard-Rho is , where is the smallest prime factor of . Whether Floyd or Brent detection is used, the expected complexity without doubling is ; with doubling, it is approximately .
It is worth noting that the preceding analysis assumes a completely random self-map, whereas Pollard-Rho uses a pseudorandom function. Therefore it has no strict complexity analysis, although it is usually fast in practice.
Example: finding the largest prime factor of a number
Example: P4718 (https://www.luogu.com.cn/problem/P4718) Pollard-Rho algorithm
For a number , use the Miller-Rabin algorithm to test primality and return immediately if it is prime. Otherwise, use Pollard-Rho to find a factor , divide by , and recursively factor and . Use Miller-Rabin to identify prime factors and update max_factor to obtain the largest one. Because the data in this problem is very large, Floyd detection is insufficient, so the doubling optimization is used.
Implementation
#include <algorithm>
#include <cstdlib>
#include <ctime>
#include <iostream>
using namespace std;
using ll = long long;
using ull = unsigned long long;
int t;
ll max_factor, n;
ll gcd(ll a, ll b) {
if (b == 0) return a;
return gcd(b, a % b);
}
ll bmul(ll a, ll b, ll m) { // Fast multiplication
ull c = (ull)a * (ull)b - (ull)((long double)a / m * b + 0.5L) * (ull)m;
if (c < (ull)m) return c;
return c + m;
}
ll qpow(ll x, ll p, ll mod) { // Fast exponentiation
ll ans = 1;
while (p) {
if (p & 1) ans = bmul(ans, x, mod);
x = bmul(x, x, mod);
p >>= 1;
}
return ans;
}
bool Miller_Rabin(ll p) { // Test primality
if (p < 2) return false;
if (p == 2) return true;
if (p == 3) return true;
ll d = p - 1, r = 0;
while (!(d & 1)) ++r, d >>= 1; // Make d odd
for (ll k = 0; k < 10; ++k) {
ll a = rand() % (p - 2) + 2;
ll x = qpow(a, d, p);
if (x == 1 || x == p - 1) continue;
for (int i = 0; i < r - 1; ++i) {
x = bmul(x, x, p);
if (x == p - 1) break;
}
if (x != p - 1) return false;
}
return true;
}
ll Pollard_Rho(ll x) {
ll s = 0, t = 0;
ll c = (ll)rand() % (x - 1) + 1;
int step = 0, goal = 1;
ll val = 1;
for (goal = 1;; goal *= 2, s = t, val = 1) { // Doubling optimization
for (step = 1; step <= goal; ++step) {
t = (bmul(t, t, x) + c) % x;
val = bmul(val, abs(t - s), x);
if ((step % 127) == 0) {
ll d = gcd(val, x);
if (d > 1) return d;
}
}
ll d = gcd(val, x);
if (d > 1) return d;
}
}
void fac(ll x) {
if (x <= max_factor || x < 2) return;
if (Miller_Rabin(x)) { // If x is prime
max_factor = max(max_factor, x); // Update the answer
return;
}
ll p = x;
while (p >= x) p = Pollard_Rho(x); // Use this algorithm
while ((x % p) == 0) x /= p;
fac(x), fac(p); // Continue factoring x and p
}
int main() {
cin >> t;
while (t--) {
srand((unsigned)time(NULL));
max_factor = 0;
cin >> n;
fac(n);
if (max_factor == n) // The largest prime factor is the number itself
cout << "Prime\n";
else
cout << max_factor << '\n';
}
return 0;
}
References and links
Footnotes
-
https://en.wikipedia.org/wiki/Birthday_problem#Reverse_problem ↩
-
Menezes, Alfred J.; van Oorschot, Paul C.; Vanstone, Scott A. (2001). Handbook of Applied Cryptography. Section 3.11 and 3.12. ↩
-
Brent, R. P. (1980), An improved Monte Carlo factorization algorithm, BIT Numerical Mathematics, 20(2): 176–184, doi:10.1007/BF01933190 ↩