Prime factorization

Introduction

Given a positive integer N∈N+N \in \mathbf{N}_{+}, try to quickly find one of its non-trivial factors.

Consider the naive algorithm. Factors occur in pairs, so all factors of NN can be divided into [2,N][2, \sqrt N] and [N+1,N)[\sqrt N+1,N). It is enough to enumerate the numbers in [2,N][2, \sqrt N] and use division to find at least two factors. This method has time complexity O(N)O(\sqrt N).

When N≥1018N\ge10^{18}, 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 NN. A lucky guess can find the answer in O(1)O(1) time, but for N≥1018N\ge10^{18} the probability of a successful guess is 11018\frac{1}{10^{18}}, and the expected number of guesses is 101810^{18}. Guessing within [2,N][2,\sqrt N] improves the success probability somewhat. We want to optimize this guessing process.

Naive algorithm

The simplest algorithm enumerates [2,N][2, \sqrt N].

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 NN NN NN NN NN NN NN KK K\sqrt K KK KK KK KK K\sqrt K KK NN pp NN pp pp pp pp

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 O(N)O(\sqrt N) to O(Nln⁡N)O(\frac {\sqrt{N}} {\ln N}). 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 O(p)=O(N)O(p)=O(\sqrt N), where pp is the smallest prime factor of NN. The Pollard-Rho algorithm introduced below is randomized and finds a non-trivial factor in expected time O(p)=O(N1/4)O(\sqrt p)=O(N^{1/4}) (Note: a non-trivial factor need not be prime).

The key idea is to take a random self-map f:Zp→Zpf: \mathbb Z_p \rightarrow \mathbb Z_p, start from any point x1x_1, and iterate xn=f(xn−1)x_n = f(x_{n-1}). The sequence enters a cycle in expected time O(p)O(\sqrt p). If we find xi≡xj(modp)x_i \equiv x_j \pmod p, then pp divides gcd⁡(∣xi−xj∣,N)\gcd(|x_i-x_j|, N), and this gcd is a non-trivial factor of NN.

The birthday paradox provides intuition for the expected time O(p)O(\sqrt p) 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 50%50\%?

Solution: suppose a year has nn days and the room has kk people, numbered 1,2,…,k1, 2,\dots, k. Assume each birthday is uniformly distributed among the nn days and that birthdays are mutually independent.

Let event kk AA AA be that the people have distinct birthdays. Its probability is

P(A)=∏i=0k−1n−inP(A)=\prod_{i=0}^{k-1}\frac{n-i}{n}

The probability that at least two people share a birthday is P(A‾)=1−P(A)P(\overline A)=1-P(A). The problem requires P(A‾)≥12P(\overline A)\ge\frac{1}{2}, so

P(A)=∏i=0k−1n−in≤12P(A)=\prod_{i=0}^{k-1}\frac{n-i}{n} \le \frac{1}{2}

Using the inequality 1+x≤ex1+x\le \mathrm{e}^x, we obtain

P(A)≤∏i=1k−1exp⁡(−in)=exp⁡(−k(k−1)2n)P(A) \le \prod_{i=1}^{k-1}\exp\left({-\frac{i}{n}}\right)=\exp \left({-\frac{k(k-1)}{2n}}\right)

Therefore

exp⁡(−k(k−1)2n)≤12  ⟹  P(A)≤12\exp\left({-\dfrac{k(k-1)}{2n}}\right) \le \frac{1}{2}\implies P(A) \le \frac{1}{2}

Substituting n=365n=365 gives k≥23k\geq 23. Thus a room needs at least 2323 people for the probability of a shared birthday to reach 50%50\%. This mathematical fact is highly counterintuitive, so it is called a paradox.

When k>56k>56 and n=365n=365, the probability of a shared birthday exceeds 99%99\%1. More generally, in a year with nn days, when a room contains 12(8nln⁡2+1+1)≈2nln⁡2\frac{1}{2}(\sqrt{8n\ln 2+1}+1)\approx \sqrt{2n\ln 2} people, the probability that at least two share a birthday is about 50%50\%.

Similarly, if birthdays are sampled independently and uniformly, the expected number of samples needed to obtain the first repeated birthday is also O(n)O(\sqrt n). Let this number be XX. Then

E(X)=∑x=1n+1P(X≥x+1)=∑x=0nn!(n−x)!nx=πn2−13+o(1).E(X) = \sum_{x=1}^{n+1}P(X\ge x+1) = \sum_{x=0}^n\frac{n!}{(n-x)!n^x} = \sqrt{\frac{\pi n}{2}}-\frac13+o(1).

This suggests that if we can sample a sequence of numbers at random, the expected sample size before a repeated number appears is also O(n)O(\sqrt n).

Finding a factor using the greatest common divisor

Constructing a random sequence modulo pp is impractical because pp is precisely what we seek. Instead, generate a pseudorandom sequence f(x)=(x2+c) mod Nf(x)=(x^2+c)\bmod N using {xi}\{x_i\}: choose x1x_1 randomly and set x2=f(x1), x3=f(x2), …, xi=f(xi−1)x_2=f(x_1),\ x_3=f(x_2),\ \dots,\ x_i=f(x_{i-1}), where c∈[1,N)c\in[1,N) 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 n=50, c=6, x1=1n=50,\ c=6,\ x_1=1 f(x)f(x), , and ; the values generated by are

1,7,5,31,17,45,31,17,45,31,…1, 7, 5, 31, 17, 45, 31, 17, 45, 31,\dots

After x4x_4, the values cycle among 31,17,4531,17,45.Arranging these values as in the figure produces a shape resembling ρ\rho, which gives the algorithm its name.

pollard-rho
pollard-rho

More importantly, this function really is a self-map on Zp\mathbb Z_p: if x≡y(modp)x\equiv y\pmod p, then f(x)≡f(y)(modp)f(x)\equiv f(y)\pmod p.

Proof

If x≡y(modp)x\equiv y\pmod p, then x2+c≡y2+c(modp)x^2+c\equiv y^2+c\pmod p. Notice that f(x)=x2+c−kxNf(x)=x^2+c-k_xN, where kxk_x is an integer depending on xx, and p∣Np|N. Thus f(x)=x2+c(modp)f(x)=x^2+c\pmod p, and consequently f(x)=f(y)(modp)f(x)=f(y)\pmod p.

As a sequence obtained by repeatedly iterating a pseudorandom self-map on Zp\mathbb Z_p, {xn mod p}\{x_n\bmod p\} repeats within expected time O(p)O(\sqrt p). Once we observe xi≡xj(modp)x_i\equiv x_j\pmod p, we can obtain a non-trivial factor of gcd⁡(∣xi−xj∣,N)\gcd(|x_i-x_j|,N) from NN. Since pp is unknown, we cannot detect the repetition directly; a simple test is that gcd⁡(∣xi−xj∣,N)\gcd(|x_i-x_j|,N) is strictly greater than one.

The algorithm does not always succeed, because gcd⁡(∣xi−xj∣,N)\gcd(|x_i-x_j|,N) may equal NN, meaning xi≡xj(modN)x_i\equiv x_j\pmod N. In that case, when {xn mod p}\{x_n\bmod p\} first repeats, {xn}\{x_n\} also repeats. No non-trivial factor is obtained. Once {xn}\{x_n\} 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 f(x)f(x) in cc.

According to the analysis above, any function ∀x≡y(modp),f(x)≡f(y)(modp)\forall x \equiv y \pmod p, f(x) \equiv f(y) \pmod p satisfying f(x)f(x) and providing sufficient pseudorandomness (for example, certain polynomial functions) can be used here. In practice, f(x)=x2+c (c≠0,−2)f(x)=x^2+c\ (c\neq 0,-2) is used most often.2

Implementation

The implementation must quickly determine during iteration whether {xn mod p}\{x_n\bmod p\} has repeated. Viewing ff as edges of a directed graph whose vertices are Zp\mathbb Z_p, we need a cycle-detection algorithm, replacing equality testing with the test whether gcd⁡(∣xi−xj∣,N)\gcd(|x_i-x_j|,N) 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 a=f(0),b=f(f(0))a=f(0),b=f(f(0)) a=f(a),b=f(f(b))a=f(a),b=f(f(b)) aa bb and . At each step update and . If and become equal during the updates, a cycle has been found.

At each step set d=gcd⁡(∣xi−xj∣,N)d=\gcd(|x_i-x_j|,N) and test whether 1<d<N1< d< N; if so, return dd. If d=Nd=N, then {xi}\{x_i\} has formed a cycle, so stop and return NN itself, adjust the random constant cc, 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 k=1k=1 from kk. In round kk, A stays still while B moves forward 2k2^k 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 ff 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 24%24\% compared with Floyd detection.

Doubling optimization

Both Floyd and Brent cycle detection use O(p)O(\sqrt p) gcd⁡\gcd gcd⁡\gcd iterations. However, computing a at every iteration slows the algorithm. Multiplicative accumulation can reduce the number of gcd computations.

In short, if gcd⁡(a,N)>1\gcd(a,N)>1, then gcd⁡(ab mod N,N)=gcd⁡(ab,N)>1\gcd(ab\bmod N,N)=\gcd(ab,N)>1 for every b∈N+b\in\mathbb N_+. Thus if gcd⁡(∏∣xi−xj∣ mod N,N)>1\gcd(\prod |x_i-x_j| \bmod N,N)>1, some pair (xi,xj)(x_i,x_j) necessarily satisfies gcd⁡(∣xi−xj∣,N)>1\gcd(|x_i-x_j|,N)>1. If the product becomes zero, factorization has failed; stop and return NN itself.

If a gcd is computed once every kk gcd⁡\gcd O(p+k−1plog⁡N)O(\sqrt p+k^{-1}\sqrt p\log N) log⁡N\log N gcd⁡\gcd kk log⁡N\log N O(p)O(\sqrt p) k=128k=128 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 O(p)O(\sqrt p), where pp is the smallest prime factor of NN. Whether Floyd or Brent detection is used, the expected complexity without doubling is O(plog⁡N)O(\sqrt p\log N); with doubling, it is approximately O(p)O(\sqrt p).

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 nn, use the Miller-Rabin algorithm to test primality and return immediately if it is prime. Otherwise, use Pollard-Rho to find a factor pp, divide nn by pp, and recursively factor nn and pp. 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;
}

Footnotes

  1. https://en.wikipedia.org/wiki/Birthday_problem#Reverse_problem ↩

  2. Menezes, Alfred J.; van Oorschot, Paul C.; Vanstone, Scott A. (2001). Handbook of Applied Cryptography. Section 3.11 and 3.12. ↩

  3. Brent, R. P. (1980), An improved Monte Carlo factorization algorithm, BIT Numerical Mathematics, 20(2): 176–184, doi:10.1007/BF01933190 ↩

This content comes from OI Wiki: Prime factorization. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.