Cairn
algorithms · number theory · O(n^(1/4)) expected

back to Number Theory

Pollard's Rho Algorithm

The site's eighth Number Theory entry, and the first to answer a genuinely different question than the other seven. Miller–Rabin and Sieve of Eratosthenes both decide whether a number is prime. Pollard's rho starts from the opposite premise — n is known composite — and asks for an actual nontrivial divisor. That's a different job: knowing 8,051 isn't prime doesn't hand you 83 and 97, and trial division (checking every candidate up to √n) is the naive way to get them, at a cost that's fine for small n and hopeless once n has dozens of digits. RSA key generation needs the first kind of test on its own huge candidates; an attacker trying to break a small or badly-generated key needs the second.

John Pollard published the algorithm in 1975. The name comes from its behavior, not its author twice over — visualize the sequence x0, x1, x2, … generated by repeatedly applying a simple pseudorandom function; because there are only finitely many values mod n, the sequence must eventually repeat, and once it does, it cycles forever. Plotted with each value as a node and each step as an arrow, the shape looks like the Greek letter ρ — a tail leading into a loop.

The birthday-paradox trick

Let p be the smallest prime factor of n (unknown to the algorithm). The sequence xi mod p also cycles — and because p is so much smaller than n, it cycles much sooner, in roughly O(√p) steps by the same birthday-paradox reasoning that makes hash collisions common far before a hash table fills up (the same argument Miller–Rabin's Rabin bound and HyperLogLog's cardinality estimate both lean on in their own ways). So long before xi mod n itself repeats, two terms xi and xj that look completely different mod n can already agree mod p — meaning p divides xi − xj even though n doesn't. Computing gcd(|xi − xj|, n) at exactly that moment finds p (or a multiple of it) without ever knowing what p is in advance.

Finding that colliding pair without storing the whole sequence is Floyd's tortoise-and-hare cycle detection: keep two pointers into the same sequence, one (the tortoise) advancing one step at a time, the other (the hare) advancing two. If there's a cycle, the hare eventually laps the tortoise — and checking gcd(|tortoise − hare|, n) after every step is enough to catch the mod-p collision the moment it happens, using O(1) extra memory.

Try it

Enter a composite n and a constant c for the pseudorandom step function f(x) = x² + c mod n, then step through the tortoise-and-hare race. 8051, c = 1 succeeds in 3 steps (83 × 97). 8051, c = 5 is the same number with a different constant — watch it run all the way to a full cycle collapse instead of finding a factor, the case covered in Pitfalls below. 4, c = 1 shows the algorithm failing outright on a tiny power of two. 9998000099, c = 2 (= 99,989 × 99,991) is a larger example — 37 steps instead of 3, but still nowhere near the ~100,000 trial division would need to reach 99,989 by brute force.

tortoise x, hare y, gcd(|x−y|, n) — oldest to newest:

Press Load, then Step through the race.

Why it works

Each step computes x = f(x) once (the tortoise) and y = f(f(y)) twice (the hare), then checks d = gcd(|x − y|, n). Three outcomes are possible:

Unlike every other entry in this category, this bound is a heuristic, not a proven worst case — it relies on f(x) = x² + c mod p behaving enough like a genuinely random function that the birthday argument applies. That assumption has no proof, but it has held up empirically for decades of use, including in real integer-factorization records.

Reference implementation

function gcd(a, b) {
  a = a < 0n ? -a : a;
  b = b < 0n ? -b : b;
  while (b) { [a, b] = [b, a % b]; }
  return a;
}

function pollardRhoOnce(n, c) {           // one attempt with a fixed constant c, x0 = 2
  const f = x => (x * x + BigInt(c)) % n;
  let x = 2n, y = 2n, d = 1n;
  while (d === 1n) {
    x = f(x);
    y = f(f(y));
    d = gcd(x > y ? x - y : y - x, n);
  }
  return d === n ? null : d;               // null = collapsed, caller should retry with a new c
}

function isPrime(n) {                      // trial division — fine for the small cofactors
  if (n < 2n) return false;                // recursion bottoms out on
  for (let i = 2n; i * i <= n; i++) if (n % i === 0n) return false;
  return true;
}

function factorize(n) {
  if (n === 1n) return [];
  if (n % 2n === 0n) return [2n, ...factorize(n / 2n)];
  if (isPrime(n)) return [n];
  let d = null, c = 1n;
  while (d === null) d = pollardRhoOnce(n, c++);   // retry with the next constant on collapse
  return [...factorize(d), ...factorize(n / d)];
}

A production factorizer would replace isPrime's trial division with Miller–Rabin — by the time rho has peeled a large number down to a plausible-prime cofactor, that cofactor can still be far too big for trial division to check quickly, even though rho itself found it fast.

Pitfalls

A run can collapse without finding a factor — that's normal, not a bug. n = 8,051 with c = 1 finds 97 in 3 steps. The exact same n with c = 5 runs the tortoise and hare through 14 steps and they land on the identical value (x = y = 4852), so |x − y| = 0 and gcd(0, 8051) = 8051 = n — a collapse, not a factor. This is checked directly, not just claimed: the demo's second preset traces exactly this run. The fix the reference implementation above uses is a plain retry loop over increasing c; c = 5 is simply an unlucky choice for this n, not evidence the algorithm is broken.

The algorithm degenerates on tiny numbers and powers of small primes. n = 4 with c = 1 collapses in 2 steps (x = y = 2, gcd(0, 4) = 4) — checked directly, and every other c from 0 to 5 fails the same way on n = 4. There simply isn't enough room in {0, 1, 2, 3} for the birthday argument to find a mod-2 collision before the tortoise and hare collide mod 4 outright. This is why the reference implementation's factorize pulls out factors of 2 with plain division before ever calling rho, and production factorizers trial-divide by a handful of small primes first for the same reason — rho is the right tool for a large, otherwise-stubborn factor, not for the small ones that trial division already finds instantly.

Rho needs to know n is composite before it starts — feeding it a prime doesn't fail loudly. Run the core loop on a prime and there's no factor to find, ever: d will eventually hit n itself (a collapse) after wasting real work, since a prime has no proper divisor greater than 1 for any collision to expose. factorize above avoids this by checking isPrime(n) before ever calling pollardRhoOnce — skipping that check doesn't produce a wrong answer so much as an expensive way to rediscover that a number was prime all along.

Complexity

O(n1/4) expected modular operations to find the smallest prime factor p of a composite n, heuristically — the birthday bound gives O(√p) steps, and in the worst case (n a product of two roughly equal primes) p ≈ √n, so O(√p) ≈ O(n1/4). Each step is one modular squaring, so the bit-operation cost is O(n1/4 log² n). That heuristic bound is why rho matters at all: for a 40-digit semiprime, √n has roughly 20 digits — trial division at that scale is astronomically infeasible — while n1/4 has roughly 10 digits, still large but searchable. It isn't the fastest known general-purpose factoring method (the quadratic sieve and general number field sieve both beat it asymptotically on large inputs), but its O(1) memory footprint and small constant make it the practical first attempt for finding small-to-medium factors before reaching for something heavier.

This site's guide, Choosing a Number Theory Algorithm, compares this entry against the other thirteen Number Theory entries side by side.