Skip to Content

Divisibilidad

Si nunca se ha encontrado teoría de números, AoPS es un buen lugar para empezar.

Recursos
FuenteRecursoNotas
AoPSAlcumus

problemas de práctica; ¡poner el foco en number theory!

AoPSIntro to NT

buen libro

Recursos

Recursos
FuenteRecursoNotas
IUSACO13.1, 13.2 - Elementary Number Theory

este módulo se basa en esto

David AltizioDivisors and Divisibility
CPH21.1 - Primes & Factors
PAPS117.1, 17.2 - Number Theory
MONT1, 3.1, and 3.2 - Divisors
AoPSNumber Theory

buenas demostraciones y problemas

Factorización prima

Un entero positivo aa se llama divisor o factor de un entero no negativo bb si bb es divisible por aa, lo que significa que existe algún entero kk tal que b=kab = ka. Un entero n>1n > 1 es primo si sus únicos divisores son 11 y nn. Los enteros mayores que 11 que no son primos son compuestos.

Todo entero positivo tiene una factorización prima única: una forma de descomponerlo en un producto de primos, como sigue:

n=p1a1p2a2pkak n = {p_1}^{a_1} {p_2}^{a_2} \cdots {p_k}^{a_k}

donde los pip_i son primos distintos y los aia_i son enteros positivos.

Ahora discutiremos cómo hallar la factorización prima de cualquier entero positivo.

vector<int> factor(int n) { vector<int> ret; for (int i = 2; i * i <= n; i++) { while (n % i == 0) { ret.push_back(i); n /= i; } } if (n > 1) { ret.push_back(n); } return ret; }
List<Integer> factor(int n) { List<Integer> factors = new ArrayList<>(); for (int i = 2; i * i <= n; i++) { while (n % i == 0) { factors.add(i); n /= i; } } if (n > 1) { factors.add(n); } return factors; }
from typing import List def factor(n: int) -> List[int]: ret = [] i = 2 while i * i <= n: while n % i == 0: ret.append(i) n //= i i += 1 if n > 1: ret.append(n) return ret

Este algoritmo corre en tiempo O(n)\mathcal{O}(\sqrt{n}), porque el bucle for comprueba divisibilidad para a lo sumo n\sqrt{n} valores. Aunque hay un bucle while dentro del bucle for, dividir nn por ii reduce rápidamente el valor de nn, lo que significa que el bucle for exterior corre menos iteraciones, lo cual de hecho acelera el código.

Veamos un ejemplo de cómo funciona este algoritmo, para n=252n = 252.

iinnret\texttt{ret}
22252252{}\{\}
22126126{2}\{2\}
226363{2,2}\{2, 2\}
332121{2,2,3}\{2, 2, 3\}
3377{2,2,3,3}\{2, 2, 3, 3\}

En este punto, el bucle for termina, porque ii ya es 3, que es mayor que 7\lfloor \sqrt{7} \rfloor. En el último paso, añadimos 77 a la lista de factores vv, porque de otro modo no se añadiría, para una factorización prima final de {2,2,3,3,7}\{2, 2, 3, 3, 7\}.

HechoFuenteNombreDificultadTagsSolución
CSESCounting DivisorsFácilen el módulo

Solución - Counting Divisors

La solución más directa es simplemente hacer lo que el problema nos pide: para cada xx, hallar el número de divisores de xx en tiempo O(x)\mathcal{O}(\sqrt x).

#include <iostream> using namespace std; int main() { int n; cin >> n; for (int q = 0; q < n; q++) { int x; int div_num = 0; cin >> x; for (int i = 1; i * i <= x; i++) { if (x % i == 0) { div_num += i * i == x ? 1 : 2; } } cout << div_num << '\n'; } }
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class Divisors { public static void main(String[] args) throws IOException { BufferedReader read = new BufferedReader(new InputStreamReader(System.in)); int queryNum = Integer.parseInt(read.readLine()); StringBuilder ans = new StringBuilder(); for (int q = 0; q < queryNum; q++) { int x = Integer.parseInt(read.readLine()); int divisors = 0; for (int i = 1; i * i <= n; i++) { if (x % i == 0) { divisors += i * i == x ? 1 : 2; } } ans.append(divisors).append('\n'); } System.out.print(ans); } }
ans = [] for _ in range(int(input())): div_num = 0 x = int(input()) i = 1 while i * i <= x: if x % i == 0: div_num += 1 if i**2 == x else 2 i += 1 ans.append(div_num) print("\n".join(str(i) for i in ans))

Esta solución corre en tiempo O(nx)\mathcal{O}(n \sqrt x), que es justo lo bastante rápida para obtener AC. Sin embargo, de hecho podemos acelerar esto para obtener una solución O((x+n)logx)\mathcal{O}((x + n) \log x).

Primero, discutamos una propiedad importante de la factorización prima. Consideremos:

x=p1a1p2a2pkak x = {p_1}^{a_1} {p_2}^{a_2} \cdots {p_k}^{a_k}

Entonces el número de divisores de xx es simplemente (a1+1)(a2+1)(ak+1)(a_1 + 1) \cdot (a_2 + 1) \cdots (a_k + 1).

¿Por qué es esto cierto? El exponente de pip_i en cualquier divisor de xx debe estar en el rango [0,ai][0, a_i] y cada exponente distinto resulta en un conjunto distinto de divisores, así que cada pip_i contribuye ai+1a_i + 1 al producto.

xx puede tener O(logx)\mathcal{O}(\log x) factores primos distintos, así que si podemos hallar la factorización prima de xx de forma eficiente, podemos usarla con la propiedad de arriba para responder consultas en tiempo O(logx)\mathcal{O}(\log x) en lugar del tiempo O(x)\mathcal{O}(\sqrt x) anterior.

Así es como hallamos la factorización prima de xx en tiempo O(logx)\mathcal{O}(\log x) con preprocesamiento O(xlogx)\mathcal{O}(x \log x):

  1. Para cada k106k \leq 10^6, hallar algún número primo que divida a kk. Para hallarlo, podemos usar la Criba de Eratóstenes  que corre en O(nlogn)\mathcal{O}(n \log n), donde nn es el mayor de los números que consideramos. También hay una versión de la criba que corre en tiempo lineal , pero no la necesitaremos.
  2. Podemos hallar la factorización prima de xx dividiéndolo repetidamente por los números primos que calculamos antes hasta que x=1x = 1.

Usar este método nos da el siguiente código:

#include <iostream> using namespace std; const int MAX_N = 1e6; // max_div[i] contains the largest prime that goes into i int max_div[MAX_N + 1]; int main() { for (int i = 2; i <= MAX_N; i++) { if (max_div[i] == 0) { for (int j = i; j <= MAX_N; j += i) { max_div[j] = i; } } } int n; cin >> n; for (int i = 0; i < n; i++) { int x; cin >> x; int div_num = 1; while (x != 1) { /* * get the largest prime that can divide x and see * how many times it goes into x (stored in count) */ int prime = max_div[x]; int count = 0; while (x % prime == 0) { count++; x /= prime; } div_num *= count + 1; } cout << div_num << '\n'; } }
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class Divisors { private static final int MAX_N = (int)Math.pow(10, 6); public static void main(String[] args) throws IOException { // maxDiv[i] contains the largest prime that can divide i int[] maxDiv = new int[MAX_N + 1]; for (int i = 2; i <= MAX_N; i++) { if (maxDiv[i] == 0) { for (int j = i; j <= MAX_N; j += i) { maxDiv[j] = i; } } } BufferedReader read = new BufferedReader(new InputStreamReader(System.in)); int queryNum = Integer.parseInt(read.readLine()); StringBuilder ans = new StringBuilder(); for (int q = 0; q < queryNum; q++) { int x = Integer.parseInt(read.readLine()); int factNum = 1; while (x != 1) { /* * get the largest prime that can divide x and see * how many times it goes into x (stored in count) */ int prime = maxDiv[x]; int count = 0; while (x % prime == 0) { count++; x /= prime; } factNum *= count + 1; } ans.append(factNum).append('\n'); } System.out.print(ans); } }
MAX_N = 10**6 # max_div[i] contains the largest prime that can go into i max_div = [0 for _ in range(MAX_N + 1)] for i in range(2, MAX_N + 1): if max_div[i] == 0: for j in range(i, MAX_N + 1, i): max_div[j] = i ans = [] for _ in range(int(input())): n = int(input()) div_num = 1 while n != 1: """ get the largest prime that can divide x and see how many times it goes into x (stored in count) """ largest = max_div[n] count = 0 while n % largest == 0: count += 1 n //= largest div_num *= count + 1 ans.append(div_num) print("\n".join(str(i) for i in ans))

MCD y MCM

MCD

El máximo común divisor (MCD / GCD) de dos enteros aa y bb es el mayor entero que es factor tanto de aa como de bb. Para hallar el MCD de dos enteros no negativos, usamos el algoritmo de Euclides, que es el siguiente:

gcd(a,b)={ab=0gcd(b,amodb)b0 \gcd(a, b) = \begin{cases} a & b = 0 \\ \gcd(b, a \bmod b) & b \neq 0 \end{cases}

Este algoritmo se puede implementar con una función recursiva como sigue:

public int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }

Para C++14, se puede usar el nativo __gcd(a,b).

En C++17, existen std::gcd y std::lcm en el header <numeric>, así que no hace falta programar el propio MCD y MCM si se usa esa versión.

def gcd(a: int, b: int) -> int: return a if b == 0 else gcd(b, a % b)

No hará falta implementar esto realmente en un contest, porque la librería nativa math tiene una función gcd y lcm.

Esta función corre en tiempo O(logab)\mathcal{O}(\log ab) porque ab    b(moda)<b2a\le b \implies b\pmod a <\frac{b}{2}.

El peor caso para el algoritmo de Euclides es cuando aa y bb son números de Fibonacci consecutivos FnF_n y Fn+1F_{n + 1}. En este caso, el algoritmo calculará que gcd(Fn,Fn+1)=gcd(Fn1,Fn)==gcd(0,F1)\gcd(F_n, F_{n + 1}) = \gcd(F_{n - 1}, F_n) = \dots = \gcd(0, F_1). Esto toma un total de n+1n+1 llamadas, que es proporcional a log(FnFn+1)\log \left(F_n F_{n+1}\right).

MCM

El mínimo común múltiplo (MCM / LCM) de dos enteros aa y bb es el menor entero divisible tanto por aa como por bb. El MCM se puede calcular con el MCD usando esta propiedad:

lcm(a,b)=abgcd(a,b) \operatorname{lcm}(a, b) = \frac{a \cdot b}{\gcd(a, b)}

Además, estas dos funciones son asociativas, lo que significa que si queremos tomar el MCD o el MCM de más de dos elementos, podemos hacerlo de a dos, en cualquier orden. Por ejemplo,

gcd(a1,a2,a3,a4)=gcd(a1,gcd(a2,gcd(a3,a4))). \gcd(a_1, a_2, a_3, a_4) = \gcd(a_1, \gcd(a_2, \gcd(a_3, a_4))).

Función φ de Euler

HechoFuenteNombreDificultadTagsSolución
SPOJETF - Euler Totient FunctionFácilen el módulo
Recursos
FuenteRecursoNotas
cp-algoEuler's Totient Function

Teoría y ejercicios

CFEuler's phi function, its properties, and how to compute it

Artículo bien cubierto

Propiedades

La función φ de Euler — escrita usando phi ϕ(n)\phi(n) — cuenta el número de enteros positivos en el intervalo [1,n][1,n] que son coprimos con nn. Dos números aa y bb son coprimos si su máximo común divisor es igual a 1, es decir, gcd(a,b)=1gcd(a,b)=1.

Aquí están los valores de ϕ(n)\phi(n) para los primeros 20 números:

n1234567891011121314151617181920
ϕ(n)\phi(n)112242646410412688166188

La función totiente es multiplicativa, lo que significa que ϕ(nm)=ϕ(n)ϕ(m)\phi(nm)=\phi(n) \cdot \phi(m), donde nn y mm son coprimos — gcd(n,m)=1gcd(n, m)=1. Por ejemplo ϕ(15)=ϕ(35)=ϕ(3)ϕ(5)=24=8\phi(15)=\phi(3 \cdot 5)=\phi(3) \cdot \phi(5) = 2 \cdot 4 = 8.

Veamos algunos casos borde de ϕ(n)\phi(n):

  • Si n es un número primo entonces ϕ(n)=n1\phi(n)=n-1 porque gcd(n,x)=1gcd(n, x)=1 para todo 1x<n1 \leq x < n
  • Si n es una potencia de un número primo, n=pqn=p^q donde p es un número primo y 1q1 \leq q entonces
    hay exactamente pq1p^{q-1} números divisibles por pp, así que ϕ(pq)=pqpq1=pq1(p1)\phi(p^q)=p^{q} - p^{q-1} = p^{q-1}(p - 1)

Usando la propiedad multiplicativa y el último caso borde podemos computar el valor de ϕ(n)\phi(n) a partir de la factorización del número nn. Sea la factorización n=p1q1p2q2pkqkn=p_1^{q_1} \cdot p_2^{q_2} \cdot \ldots \cdot p_k^{q_k} donde pip_i es un factor primo de nn, entonces:

ϕ(n)=ϕ(p1q1)ϕ(p2q2)ϕ(pkqk)=p1q11(p11)p2q21(p21)pkqk1(qk1) \phi(n)=\phi(p_1^{q_1}) \cdot \phi(p_2^{q_2}) \cdot \ldots \cdot \phi(p_k^{q_k}) = p_1^{q_1-1}(p_1 - 1) \cdot p_2^{q_2-1}(p_2 - 1) \cdot \ldots \cdot p_k^{q_k-1}(q_k - 1)

Abajo hay una implementación para factorizar en O(n)\mathcal{O}(\sqrt{n}). Puede ser un poco complicado entender por qué restamos ans/pans/p de ansans. Por ejemplo ans=pqxans=p^q \cdot x, donde pp es un factor primo y xx es el resto de la factorización prima. Al restar ansp=pq1x\frac{ans}{p}=p^{q-1} \cdot x terminamos con: pqxpq1x=pq1x(p1)p^q \cdot x - p^{q-1} \cdot x = p^{q-1} \cdot x \cdot (p - 1), que es exactamente la forma de ϕ(n)\phi(n) descrita unas líneas arriba.

int phi(int n) { int ans = n; for (int p = 2; p * p <= n; p++) { if (n % p == 0) { while (n % p == 0) { n /= p; } ans -= ans / p; } } if (n > 1) { ans -= ans / n; } return ans; }
public static int phi(int n) { int ans = n; for (int p = 2; p * p <= n; p++) { if (n % p == 0) { while (n % p == 0) { n /= p; } ans -= ans / p; } } if (n > 1) { ans -= ans / n; } return ans; }
def phi(n: int) -> int: ans = n p = 2 while p * p <= n: if n % p == 0: while n % p == 0: n //= p ans -= ans // p p += 1 if n > 1: ans -= ans // n return ans

Por lo general en los problemas necesitamos precomputar el totiente de todos los números entre 11 y nn; entonces factorizar no es eficiente. La idea es la misma que la Criba de Eratóstenes. Como es casi lo mismo que la Criba de Eratóstenes, la complejidad temporal será: O(NloglogN)\mathcal{O}(N\log\log{N}).

void precompute() { for (int i = 1; i < MAX_N; i++) { phi[i] = i; } for (int i = 2; i < MAX_N; i++) { // If i is prime if (phi[i] == i) { for (int j = i; j < MAX_N; j += i) { phi[j] -= phi[j] / i; } } } }
public static void precompute() { for (int i = 1; i < MAX_N; i++) { phi[i] = i; } for (int i = 2; i < MAX_N; i++) { // If i is prime if (phi[i] == i) { for (int j = i; j < MAX_N; j += i) { phi[j] -= phi[j] / i; } } } }
def precompute(): for i in range(1, MAX_N): phi[i] = i for i in range(2, MAX_N): # If i is prime if phi[i] == i: for j in range(i, MAX_N, i): phi[j] -= phi[j] // i
HechoFuenteNombreDificultadTagsSolución
SPOJGCDEX - GCD ExtremeDifícilen el módulo

Solución

Se nos pide computar la suma de MCDs de todos los pares (i,j)(i, j) tales que 1i<jn1 \le i < j \le n:

i=1nj=i+1ngcd(i,j). \sum_{i=1}^{n} \sum_{j=i+1}^{n} \gcd(i,j).

Definamos una función auxiliar f(n)f(n) que computa la suma de MCDs para un segundo elemento fijo nn:

f(n)=i=1ngcd(i,n). f(n) = \sum_{i=1}^{n} \gcd(i, n).

Esta función suma gcd(i,n)\gcd(i, n) para todo ini \le n. Los términos donde gcd(i,n)=d\gcd(i, n) = d son exactamente aquellos donde dd divide a nn y gcd(id,nd)=1\gcd(\frac{i}{d}, \frac{n}{d}) = 1. El número de tales enteros ii lo da la función φ de Euler ϕ(nd)\phi(\frac{n}{d}). Así, podemos reescribir f(n)f(n) como una suma sobre los divisores de nn:

f(n)=dndϕ(nd). f(n) = \sum_{d|n} d \cdot \phi\left(\frac{n}{d}\right).

Podemos computar f(n)f(n) para todo nn hasta 10610^6 de forma eficiente. En lugar de factorizar cada número, iteramos por cada posible divisor ii y actualizamos todos sus múltiplos jj. Para un ii fijo, añadimos la contribución iϕ(ji)i \cdot \phi(\frac{j}{i}) a f(j)f(j) para todos los jj que son múltiplos de ii.

Finalmente, el problema pide pares donde i<ji < j. El valor f(j)f(j) incluye el caso i=ji=j (donde gcd(j,j)=j\gcd(j, j) = j), que debemos excluir. La respuesta para un nn dado es la suma de prefijos de estos valores ajustados:

ans[n]=j=1n(f(j)j). \text{ans}[n] = \sum_{j=1}^{n} (f(j) - j).

La complejidad total de esta precomputación está acotada por la suma de la serie armónica:

i=1NNiNlnN\sum_{i=1}^{N} \frac{N}{i} \approx N \ln N
#include <bits/stdc++.h> using namespace std; using ll = long long; const int MAX_N = 1e6; ll phi[MAX_N + 1]; ll f[MAX_N + 1]; ll sum[MAX_N + 1]; int main() { // compute phi using sieve for (int i = 1; i <= MAX_N; i++) phi[i] = i; for (int i = 2; i <= MAX_N; i++) if (phi[i] == i) for (int j = i; j <= MAX_N; j += i) phi[j] -= phi[j] / i; // compute f[j] for (int i = 1; i <= MAX_N; i++) for (int j = i; j <= MAX_N; j += i) f[j] += 1LL * i * phi[j / i]; // prefix sums for answers (remove diagonal a=b) for (int i = 1; i <= MAX_N; i++) sum[i] = sum[i - 1] + f[i] - i; int n; while (cin >> n) { if (n == 0) break; cout << sum[n] << '\n'; } return 0; }

Problemas

HechoFuenteNombreDificultadTagsSolución
ACDiv GameFácilPrime FactorizationSolución
CFProduct 1 Modulo NFácilDivisibility, Modular ArithmeticSolución
CFPower ProductsFácilNTSolución
CFDiluc and KaeyaFácilDivisibilitySolución
CSESPermutation RoundsFácilFunctional Graph, Prime FactorizationSolución
SPOJNAJPWG - Playing with GCDFácilDivisibilitySolución
CSESCommon DivisorsNormalDivisibilitySolución
CFOrac and LCMNormalPrime FactorizationSolución
CCMaximum of GCDsNormalDivisibilitySolución
CSESSum of DivisorsDifícilDivisibilitySolución
SPOJLCM SumDifícilDivisibility, LCM, Euler TotientSolución
CFThe Number of PairsDifícilDivisibility
ACsqrt(n²+n+X)DifícilDivisibility