Divisibilidad
Si nunca se ha encontrado teoría de números, AoPS es un buen lugar para empezar.
| Fuente | Recurso | Notas |
|---|---|---|
| AoPS | Alcumus | problemas de práctica; ¡poner el foco en number theory! |
| AoPS | Intro to NT | buen libro |
Recursos
| Fuente | Recurso | Notas |
|---|---|---|
| IUSACO | 13.1, 13.2 - Elementary Number Theory | este módulo se basa en esto |
| David Altizio | Divisors and Divisibility | |
| CPH | 21.1 - Primes & Factors | |
| PAPS1 | 17.1, 17.2 - Number Theory | |
| MONT | 1, 3.1, and 3.2 - Divisors | |
| AoPS | Number Theory | buenas demostraciones y problemas |
Factorización prima
Un entero positivo se llama divisor o factor de un entero no negativo si es divisible por , lo que significa que existe algún entero tal que . Un entero es primo si sus únicos divisores son y . Los enteros mayores que 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:
donde los son primos distintos y los 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 retEste algoritmo corre en tiempo , porque el bucle for comprueba divisibilidad para a lo sumo valores. Aunque hay un bucle while dentro del bucle for, dividir por reduce rápidamente el valor de , 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 .
En este punto, el bucle for termina, porque ya es 3, que es mayor que . En el último paso, añadimos a la lista de factores , porque de otro modo no se añadiría, para una factorización prima final de .
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CSES | Counting Divisors | Fácil | en el módulo |
Solución - Counting Divisors
La solución más directa es simplemente hacer lo que el problema nos pide: para cada , hallar el número de divisores de en tiempo .
#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 , que es justo lo bastante rápida para obtener AC. Sin embargo, de hecho podemos acelerar esto para obtener una solución .
Primero, discutamos una propiedad importante de la factorización prima. Consideremos:
Entonces el número de divisores de es simplemente .
¿Por qué es esto cierto? El exponente de en cualquier divisor de debe estar en el rango y cada exponente distinto resulta en un conjunto distinto de divisores, así que cada contribuye al producto.
puede tener factores primos distintos, así que si podemos hallar la factorización prima de de forma eficiente, podemos usarla con la propiedad de arriba para responder consultas en tiempo en lugar del tiempo anterior.
Así es como hallamos la factorización prima de en tiempo con preprocesamiento :
- Para cada , hallar algún número primo que divida a . Para hallarlo, podemos usar la Criba de Eratóstenes que corre en , donde 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.
- Podemos hallar la factorización prima de dividiéndolo repetidamente por los números primos que calculamos antes hasta que .
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 y es el mayor entero que es factor tanto de como de . Para hallar el MCD de dos enteros no negativos, usamos el algoritmo de Euclides, que es el siguiente:
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 porque .
El peor caso para el algoritmo de Euclides es cuando y son números de Fibonacci consecutivos y . En este caso, el algoritmo calculará que . Esto toma un total de llamadas, que es proporcional a .
MCM
El mínimo común múltiplo (MCM / LCM) de dos enteros y es el menor entero divisible tanto por como por . El MCM se puede calcular con el MCD usando esta propiedad:
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,
Función φ de Euler
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| SPOJ | ★ ETF - Euler Totient Function | Fácil | en el módulo |
| Fuente | Recurso | Notas |
|---|---|---|
| cp-algo | Euler's Totient Function | Teoría y ejercicios |
| CF | Euler's phi function, its properties, and how to compute it | Artículo bien cubierto |
Propiedades
La función φ de Euler — escrita usando phi — cuenta el número de enteros positivos en el intervalo que son coprimos con . Dos números y son coprimos si su máximo común divisor es igual a 1, es decir, .
Aquí están los valores de para los primeros 20 números:
| n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 2 | 2 | 4 | 2 | 6 | 4 | 6 | 4 | 10 | 4 | 12 | 6 | 8 | 8 | 16 | 6 | 18 | 8 |
La función totiente es multiplicativa, lo que significa que , donde y son coprimos — . Por ejemplo .
Veamos algunos casos borde de :
- Si n es un número primo entonces porque para todo
- Si n es una potencia de un número primo, donde p es un número
primo y entonces
hay exactamente números divisibles por , así que
Usando la propiedad multiplicativa y el último caso borde podemos computar el valor de a partir de la factorización del número . Sea la factorización donde es un factor primo de , entonces:
Abajo hay una implementación para factorizar en . Puede ser un poco complicado entender por qué restamos de . Por ejemplo , donde es un factor primo y es el resto de la factorización prima. Al restar terminamos con: , que es exactamente la forma de 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 ansPor lo general en los problemas necesitamos precomputar el totiente de todos los números entre y ; 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á: .
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| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| SPOJ | ★ GCDEX - GCD Extreme | Difícil | en el módulo |
Solución
Se nos pide computar la suma de MCDs de todos los pares tales que :
Definamos una función auxiliar que computa la suma de MCDs para un segundo elemento fijo :
Esta función suma para todo . Los términos donde son exactamente aquellos donde divide a y . El número de tales enteros lo da la función φ de Euler . Así, podemos reescribir como una suma sobre los divisores de :
Podemos computar para todo hasta de forma eficiente. En lugar de factorizar cada número, iteramos por cada posible divisor y actualizamos todos sus múltiplos . Para un fijo, añadimos la contribución a para todos los que son múltiplos de .
Finalmente, el problema pide pares donde . El valor incluye el caso (donde ), que debemos excluir. La respuesta para un dado es la suma de prefijos de estos valores ajustados:
La complejidad total de esta precomputación está acotada por la suma de la serie armónica:
#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
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| AC | Div Game | Fácil | Prime Factorization | Solución | |
| CF | Product 1 Modulo N | Fácil | Divisibility, Modular Arithmetic | Solución | |
| CF | Power Products | Fácil | NT | Solución | |
| CF | Diluc and Kaeya | Fácil | Divisibility | Solución | |
| CSES | ★ Permutation Rounds | Fácil | Functional Graph, Prime Factorization | Solución | |
| SPOJ | ★ NAJPWG - Playing with GCD | Fácil | Divisibility | Solución | |
| CSES | ★ Common Divisors | Normal | Divisibility | Solución | |
| CF | Orac and LCM | Normal | Prime Factorization | Solución | |
| CC | ★ Maximum of GCDs | Normal | Divisibility | Solución | |
| CSES | Sum of Divisors | Difícil | Divisibility | Solución | |
| SPOJ | LCM Sum | Difícil | Divisibility, LCM, Euler Totient | Solución | |
| CF | The Number of Pairs | Difícil | Divisibility | — | |
| AC | sqrt(n²+n+X) | Difícil | Divisibility | — |