Factorización de enteros
En este artículo listamos varios algoritmos para la factorización de enteros, cada uno de los cuales puede ser rápido o lento en distintos grados según su entrada.
Nótese que, si el número que se quiere factorizar es en realidad un número primo, la mayoría de los algoritmos se ejecutarán muy lentamente. Esto es especialmente cierto para los algoritmos de factorización de Fermat, p−1 de Pollard y rho de Pollard. Por lo tanto, lo más razonable es realizar un test de primalidad probabilístico (o uno determinista rápido) antes de intentar factorizar el número.
División por tentativa
Este es el algoritmo más básico para hallar una factorización prima.
Dividimos por cada divisor posible . Se puede observar que es imposible que todos los factores primos de un número compuesto sean mayores que . Por lo tanto, solo hay que probar los divisores , lo que nos da la factorización prima en . (Esto es tiempo seudopolinomial , es decir, polinomial en el valor de la entrada pero exponencial en la cantidad de bits de la entrada.)
El menor divisor debe ser un número primo. Quitamos el número factorizado y continuamos el proceso. Si no podemos hallar ningún divisor en el rango , entonces el número mismo tiene que ser primo.
vector<long long> trial_division1(long long n) {
vector<long long> factorization;
for (long long d = 2; d * d <= n; d++) {
while (n % d == 0) {
factorization.push_back(d);
n /= d;
}
}
if (n > 1)
factorization.push_back(n);
return factorization;
}Factorización por rueda (wheel factorization)
Esta es una optimización de la división por tentativa. Una vez que sabemos que el número no es divisible por 2, no hace falta comprobar otros números pares. Esto nos deja solo el de los números para comprobar. Después de extraer el factor 2 y obtener un número impar, podemos empezar simplemente en 3 y contar solo los demás números impares.
vector<long long> trial_division2(long long n) {
vector<long long> factorization;
while (n % 2 == 0) {
factorization.push_back(2);
n /= 2;
}
for (long long d = 3; d * d <= n; d += 2) {
while (n % d == 0) {
factorization.push_back(d);
n /= d;
}
}
if (n > 1)
factorization.push_back(n);
return factorization;
}Este método se puede extender más. Si el número no es divisible por 3, también podemos ignorar todos los demás múltiplos de 3 en los cálculos futuros. Así que solo hay que comprobar los números . Podemos observar un patrón en estos números restantes. Hay que comprobar todos los números con y . Así nos queda solo el de los números para comprobar. Podemos implementar esto extrayendo primero los primos 2 y 3, después de lo cual empezamos en 5 y solo contamos restos y módulo .
Aquí hay una implementación para los números primos 2, 3 y 5. Es conveniente guardar los incrementos de salto en un arreglo.
vector<long long> trial_division3(long long n) {
vector<long long> factorization;
for (int d : {2, 3, 5}) {
while (n % d == 0) {
factorization.push_back(d);
n /= d;
}
}
static array<int, 8> increments = {4, 2, 4, 2, 4, 6, 2, 6};
int i = 0;
for (long long d = 7; d * d <= n; d += increments[i++]) {
while (n % d == 0) {
factorization.push_back(d);
n /= d;
}
if (i == 8)
i = 0;
}
if (n > 1)
factorization.push_back(n);
return factorization;
}Si seguimos extendiendo este método para incluir aún más primos, se pueden alcanzar mejores porcentajes, pero las listas de saltos se volverán más grandes.
Primos precomputados
Extendiendo el método de factorización por rueda de forma indefinida, solo nos quedarán números primos para comprobar. Una buena forma de comprobar esto es precomputar todos los números primos con la Criba de Eratóstenes hasta , y probarlos individualmente.
vector<long long> primes;
vector<long long> trial_division4(long long n) {
vector<long long> factorization;
for (long long d : primes) {
if (d * d > n)
break;
while (n % d == 0) {
factorization.push_back(d);
n /= d;
}
}
if (n > 1)
factorization.push_back(n);
return factorization;
}Método de factorización de Fermat
Podemos escribir un número compuesto impar como la diferencia de dos cuadrados :
El método de factorización de Fermat intenta aprovechar este hecho adivinando el primer cuadrado , y comprobando si la parte restante, , también es un cuadrado perfecto. Si lo es, entonces hemos hallado los factores y de .
int fermat(int n) {
int a = ceil(sqrt(n));
int b2 = a*a - n;
int b = round(sqrt(b2));
while (b * b != b2) {
a = a + 1;
b2 = a*a - n;
b = round(sqrt(b2));
}
return a - b;
}Este método de factorización puede ser muy rápido si la diferencia entre los dos factores y es pequeña. El algoritmo se ejecuta en tiempo . En la práctica, sin embargo, este método se usa rara vez. Una vez que los factores se alejan más, es extremadamente lento.
Sin embargo, todavía hay una gran cantidad de opciones de optimización respecto a este enfoque. Mirando los cuadrados módulo un número pequeño fijo, se puede observar que ciertos valores no tienen que considerarse, ya que no pueden producir un cuadrado perfecto .
Método de Pollard { data-toc-label=“Método de Pollard” }
Es muy probable que un número tenga al menos un factor primo tal que sea -powersmooth para un pequeño. Se dice que un entero es -powersmooth (B-potencia-suave) si toda potencia de primo que divide a es a lo sumo . De forma formal, sea y sea cualquier entero positivo. Supongamos que la factorización prima de es , donde cada es un primo y . Entonces es -powersmooth si, para todo , . P. ej. la factorización prima de es . Y los valores y son -powersmooth y -powersmooth respectivamente, porque y . En 1974 John Pollard inventó un método para extraer factores , t.q. es -powersmooth, de un número compuesto.
La idea viene del pequeño teorema de Fermat. Sea una factorización de . El teorema afirma que si es coprimo con , se cumple lo siguiente:
Esto también significa que
Así, para cualquier con sabemos que . Esto significa que , y por eso también .
Por lo tanto, si para un factor de divide a , podemos extraer un factor usando el algoritmo de Euclides.
Está claro que el menor que es múltiplo de todo número -powersmooth es . O, de forma alternativa:
Nótese que, si divide a para todos los factores primos de , entonces será simplemente . En este caso no obtenemos un factor. Por lo tanto, intentaremos realizar el varias veces, mientras computamos .
Algunos números compuestos no tienen factores t.q. sea -powersmooth para un pequeño. Por ejemplo, para el número compuesto , los valores son -powersmooth y -powersmooth respectivamente. Tendremos que elegir para factorizar el número.
En la siguiente implementación empezamos con y aumentamos después de cada iteración.
long long pollards_p_minus_1(long long n) {
int B = 10;
long long g = 1;
while (B <= 1000000 && g < n) {
long long a = 2 + rand() % (n - 3);
g = gcd(a, n);
if (g > 1)
return g;
// calcula a^M
for (int p : primes) {
if (p >= B)
continue;
long long p_power = 1;
while (p_power * p <= B)
p_power *= p;
a = power(a, p_power, n);
g = gcd(a - 1, n);
if (g > 1 && g < n)
return g;
}
B *= 2;
}
return 1;
}
Obsérvese que este es un algoritmo probabilístico. Una consecuencia de esto es que existe la posibilidad de que el algoritmo no sea capaz de hallar un factor en absoluto.
La complejidad es por iteración.
Algoritmo rho de Pollard
El algoritmo rho de Pollard es otro algoritmo de factorización de John Pollard.
Sea la factorización prima de un número. El algoritmo observa una secuencia seudoaleatoria donde es una función polinómica; usualmente se elige con .
En este caso, no nos interesa la secuencia . Nos interesa más la secuencia . Como es una función polinómica, y todos los valores están en el rango , esta secuencia eventualmente convergerá a un bucle. La paradoja del cumpleaños de hecho sugiere que la cantidad esperada de elementos es hasta que empieza la repetición. Si es menor que , la repetición probablemente empezará en .
Aquí hay una visualización de una secuencia de ese tipo con , , y . Por la forma de la secuencia se ve con mucha claridad por qué el algoritmo se llama algoritmo de Pollard.
Aun así, queda una pregunta abierta. ¿Cómo podemos aprovechar las propiedades de la secuencia a nuestro favor sin siquiera conocer el número mismo?
En realidad es bastante fácil. Hay un ciclo en la secuencia si y solo si hay dos índices tales que . Esta ecuación se puede reescribir como , que es lo mismo que .
Por lo tanto, si hallamos dos índices y con , hemos hallado un ciclo y también un factor de . Es posible que . En este caso no hemos hallado un factor propio, así que hay que repetir el algoritmo con un parámetro distinto (distinto valor inicial , distinta constante en la función polinómica ).
Para hallar el ciclo, podemos usar cualquier algoritmo común de detección de ciclos.
Algoritmo de detección de ciclos de Floyd
Este algoritmo halla un ciclo usando dos punteros que recorren la secuencia a distintas velocidades. En cada iteración, el primer puntero avanzará un elemento, mientras que el segundo puntero avanza de a dos elementos. Usando esta idea es fácil observar que, si hay un ciclo, en algún momento el segundo puntero dará la vuelta y se encontrará con el primero durante los bucles. Si la longitud del ciclo es y es el primer índice en el que empieza el ciclo, entonces el algoritmo se ejecutará en tiempo .
Este algoritmo también se conoce como el algoritmo de la tortuga y la liebre, basado en el cuento en el que una tortuga (el puntero lento) y una liebre (el puntero más rápido) tienen una carrera.
De hecho es posible determinar los parámetros y usando este algoritmo (también en tiempo y espacio ). Cuando se detecta un ciclo, el algoritmo devolverá ‘True’. Si la secuencia no tiene un ciclo, entonces la función se quedará en un bucle infinito. Sin embargo, usando el algoritmo rho de Pollard, esto se puede evitar.
function floyd(f, x0):
tortoise = x0
hare = f(x0)
while tortoise != hare:
tortoise = f(tortoise)
hare = f(f(hare))
return trueImplementación
Primero, aquí hay una implementación que usa el algoritmo de detección de ciclos de Floyd. El algoritmo en general se ejecuta en tiempo .
long long mult(long long a, long long b, long long mod) {
return (__int128)a * b % mod;
}
long long f(long long x, long long c, long long mod) {
return (mult(x, x, mod) + c) % mod;
}
long long rho(long long n, long long x0=2, long long c=1) {
long long x = x0;
long long y = x0;
long long g = 1;
while (g == 1) {
x = f(x, c, n);
y = f(y, c, n);
y = f(y, c, n);
g = gcd(abs(x - y), n);
}
return g;
}La siguiente tabla muestra los valores de e durante el algoritmo para , y .
La implementación usa una función mult, que multiplica dos enteros sin desbordamiento usando el tipo __int128 de GCC para enteros de 128 bits.
Si GCC no está disponible, se puede usar una idea similar a la exponenciación binaria.
long long mult(long long a, long long b, long long mod) {
long long result = 0;
while (b) {
if (b & 1)
result = (result + a) % mod;
a = (a + a) % mod;
b >>= 1;
}
return result;
}Como alternativa, también se puede implementar la multiplicación de Montgomery.
Como se indicó antes, si es compuesto y el algoritmo devuelve como factor, hay que repetir el procedimiento con distintos parámetros y . P. ej. la elección no factorizará . El algoritmo devolverá . Sin embargo, la elección , sí lo factorizará.
Algoritmo de Brent
Brent implementa un método similar al de Floyd, usando dos punteros. La diferencia es que, en lugar de avanzar los punteros de a uno y de a dos lugares respectivamente, se avanzan por potencias de dos. En cuanto es mayor que y , hallaremos el ciclo.
function floyd(f, x0):
tortoise = x0
hare = f(x0)
l = 1
while tortoise != hare:
tortoise = hare
repeat l times:
hare = f(hare)
if tortoise == hare:
return true
l *= 2
return trueEl algoritmo de Brent también se ejecuta en tiempo lineal, pero en general es más rápido que el de Floyd, ya que usa menos evaluaciones de la función .
Implementación
La implementación directa del algoritmo de Brent se puede acelerar omitiendo los términos si . Además, en lugar de realizar el cálculo del en cada paso, multiplicamos los términos y solo comprobamos realmente el cada ciertos pasos, y retrocedemos si nos pasamos.
long long brent(long long n, long long x0=2, long long c=1) {
long long x = x0;
long long g = 1;
long long q = 1;
long long xs, y;
int m = 128;
int l = 1;
while (g == 1) {
y = x;
for (int i = 1; i < l; i++)
x = f(x, c, n);
int k = 0;
while (k < l && g == 1) {
xs = x;
for (int i = 0; i < m && i < l - k; i++) {
x = f(x, c, n);
q = mult(q, abs(y - x), n);
}
g = gcd(q, n);
k += m;
}
l *= 2;
}
if (g == n) {
do {
xs = f(xs, c, n);
g = gcd(abs(xs - y), n);
} while (g == 1);
}
return g;
}La combinación de una división por tentativa para números primos pequeños junto con la versión de Brent del algoritmo rho de Pollard da un algoritmo de factorización muy potente.