Sumas de prefijos de funciones aritméticas (Parte 1)
En este módulo introducimos cómo calcular sumas de prefijos de ciertas funciones aritméticas en tiempo sublineal. Aquí hay algunos ejemplos:
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CSES | Sum of Divisors | Fácil | Solución |
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| YS | Sum of Totient Function | Normal | en el módulo |
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| YS | Counting Primes | Normal | en el módulo |
Este módulo (parte 1) se enfocará en temas relacionados con los primeros dos problemas foco. El conteo de primos y aplicaciones relacionadas se pospondrán a la parte 2.
Funciones multiplicativas
Las funciones sobre las que quisiéramos calcular sumas de prefijos en los primeros dos problemas foco son ambas multiplicativas.
Definición
- Si una función mapea enteros positivos a números complejos, es una función aritmética.
- Si es una función aritmética, y para cualesquiera enteros positivos coprimos , , es una función multiplicativa.
- Si es multiplicativa y = para cualesquiera enteros positivos , , es una función completamente multiplicativa.
Si es una función multiplicativa, entonces para un entero positivo , tenemos
Si es una función completamente multiplicativa, entonces para un entero positivo , tenemos
Ejemplos
Funciones multiplicativas comunes son
- Función suma de divisores: , que representa la suma de las -ésimas potencias de los divisores de . Observar que y son distintas.
- Función conteo de divisores: , que representa el conteo de divisores de , también denotada .
- Función suma de divisores: , que representa la suma de los divisores de .
- Función φ de Euler: , que representa el conteo de enteros positivos menores o iguales que y coprimos con . Además, , es par.
- Función de Möbius: , que sirve como el inverso multiplicativo de la función identidad en la convolución de Dirichlet, , para un número libre de cuadrados , , y para un número con factores cuadrados, .
- Función unidad: , que sirve como el elemento identidad en la convolución de Dirichlet, completamente multiplicativa.
- Función constante: , completamente multiplicativa.
- Función identidad: , completamente multiplicativa.
- Función potencia: , completamente multiplicativa.
Las dos fórmulas clásicas respecto de la función de Möbius y la función de Euler son:
- , interpretando como los coeficientes del principio de inclusión-exclusión lo demuestra.
- . Para demostrarlo, podemos contar el número de ocurrencias de en su forma de fracción más simple.
Recursos
Como este módulo pretende servir como una introducción suave a este tema, usualmente describiremos solo la solución más simple que pasa las restricciones dadas. Soluciones con complejidades temporales asintóticamente mejores se pueden encontrar en blogs como los siguientes, aunque hay que tener en cuenta que pueden no ser mucho más rápidas bajo las restricciones dadas.
| Fuente | Recurso | Notas |
|---|---|---|
| CF | [Tutorial] Math note — Möbius inversion | |
| CF | [Tutorial] Math note — Dirichlet convolution | |
| CF | Dirichlet convolution. Part 1: Fast prefix sum computations | |
| CF | Catalog | ver la sección de teoría de números para posts de blog sobre temas relacionados |
Calentamiento
Empecemos introduciendo un conjunto de números con el que a menudo trabajamos al calcular sumas de prefijos de funciones multiplicativas hasta .
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| YS | Enumerate Quotients | Fácil | en el módulo |
Ejercicio para el lector: ¿qué tan grande puede ser este conjunto?
Respuesta
Sea . Entonces el conjunto puede tener tamaño a lo sumo ya que cada elemento es o bien a lo sumo , o de la forma para algún .
Una propiedad importante de este conjunto es que si entonces .
¿Por qué?
Implementación
Código
Complejidad temporal:
#include <bits/stdc++.h>
using namespace std;
template <class T> using V = vector<T>;
#define all(x) begin(x), end(x)
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
ll N;
cin >> N;
auto square = [&](ll s) { return s * s; };
ll s = 1;
while (square(s + 1) <= N) ++s;
V<ll> a;
for (ll i = 1; i <= s; ++i) a.push_back(i);
for (ll i = s; i >= 1; --i) {
if (i == s && a.back() == N / i) continue;
a.push_back(N / i);
}
int k = size(a);
cout << k << "\n";
for (int i = 0; i < k; ++i) cout << a[i] << " \n"[i + 1 == k];
}Ejemplo - Suma de divisores
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CSES | Sum of Divisors | Fácil | Solución |
Pista: la complejidad temporal es la misma que la del problema anterior.
Solución
Explicación
No es factible calcularlo de forma directa, pero podemos reescribir la suma así:
Cuando , hay solo valores distintos para . De forma similar, cuando , tiene solo valores distintos. Para un fijo, los valores de forman un intervalo contiguo, que es . Podemos calcular cada una de estas contribuciones en .
Notas adicionales:
- La suma del número de divisores de los primeros enteros positivos se puede calcular de la misma manera.
- . Esto es cierto porque ambas cuentan , donde la primera suma recorre y la segunda recorre .
Implementación
Código
Complejidad temporal:
Usamos la clase Modint de AtCoder para simplificar la implementación.
#include <atcoder/modint>
#include <bits/stdc++.h>
using namespace std;
using namespace atcoder;
using mint = modint1000000007;
template <class T> using V = vector<T>;
#define all(x) begin(x), end(x)
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
ll n;
cin >> n;
const mint i2 = mint(1) / 2;
auto arith = [&](mint l, mint r) { return (l + r) * (r - l + 1) * i2; };
mint ans = 0;
for (ll l = 1; l <= n;) {
ll q = n / l;
ll r = n / q;
ans += arith(l, r) * mint(q);
l = r + 1;
}
cout << ans.val() << "\n";
}Convolución de Dirichlet
Introducción
La convolución de Dirichlet de funciones aritméticas y se define como . La convolución de Dirichlet cumple conmutatividad, asociatividad y distributividad respecto de la suma. Existe una función identidad tal que 。Si y son funciones multiplicativas, entonces también es multiplicativa.
Una técnica común con la convolución de Dirichlet involucra lidiar con la convolución de una función y la función identidad . Por ejemplo, si y , entonces . Si es multiplicativa, entonces tenemos
Si queremos recuperar a partir de , podemos escribir . Es decir, . Esto se conoce como inversión de Möbius.
Ejemplo - Convolución de Dirichlet y sumas de prefijos
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| YS | Dirichlet Convolution and Prefix Sums | Normal | en el módulo |
Dados y , si definimos , podemos calcular en tiempo sublineal.
Solución
Explicación
Para podemos calcular en ya que si , entonces o bien o ( es chico o es chico). Podemos contabilizar ambos casos chicos y luego restar su superposición.
Además, .
Bonus: es posible reducir esto a (ver este comentario ).
Implementación
Código
Complejidad temporal:
#include <atcoder/modint>
#include <bits/stdc++.h>
using namespace std;
using namespace atcoder;
using mint = modint998244353;
template <class T> using V = vector<T>;
#define all(x) begin(x), end(x)
using ll = long long;
ll sq(ll x) { return x * x; }
// BeginCodeSnip{Dirichlet Convolution}
struct Dirichlet {
ll N;
int s = 0, k;
Dirichlet(ll N_) : N(N_) {
while (sq(s + 1) <= N) ++s;
k = 2 * s - (s == N / s);
}
ll pos_to_val(int i) {
if (i < s) return i + 1;
return N / (k - i);
}
int val_to_pos(ll l) {
if (l <= s) return l - 1;
return k - N / l;
}
V<mint> convolve(const V<mint> &f, const V<mint> &g) {
V<mint> h(k);
for (int i = 0; i < k; ++i) {
ll v = pos_to_val(i);
ll j = 1;
for (; j * j <= v; ++j) {
int p = val_to_pos(v / j);
h[i] += (f[j - 1] - (j == 1 ? 0 : f[j - 2])) * g[p];
h[i] += (g[j - 1] - (j == 1 ? 0 : g[j - 2])) * f[p];
}
--j;
assert(j > 0);
h[i] -= f[j - 1] * g[j - 1];
}
return h;
}
};
// EndCodeSnip{}
void solve() {
ll N;
cin >> N;
Dirichlet dc(N);
int k = dc.k;
V<mint> f(k), g(k);
for (auto &t : f) {
int v;
cin >> v;
t = v;
}
for (auto &t : g) {
int v;
cin >> v;
t = v;
}
auto ret = dc.convolve(f, g);
for (int i = 0; i < k; ++i) { cout << ret.at(i).val() << " \n"[i + 1 == k]; }
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) solve();
}Ejemplo - Inverso de Dirichlet y sumas de prefijos
Supongamos que en lugar de hallar dados y , queremos recuperar dados y .
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| YS | Dirichlet Inverse and Prefix Sums | Normal | en el módulo |
Solución
Explicación
Similar al problema anterior: iteramos sobre en orden creciente, pero en lugar de derivar de y podemos derivar de , y .
Implementación
Código
Complejidad temporal:
#include <atcoder/modint>
#include <bits/stdc++.h>
using namespace std;
using namespace atcoder;
using mint = modint998244353;
template <class T> using V = vector<T>;
#define all(x) begin(x), end(x)
using ll = long long;
ll sq(ll x) { return x * x; }
// BeginCodeSnip{Dirichlet Inverse}
struct Dirichlet {
ll N;
int s = 0, k;
Dirichlet(ll N_) : N(N_) {
while (sq(s + 1) <= N) ++s;
k = 2 * s - (s == N / s);
}
ll pos_to_val(int i) {
if (i < s) return i + 1;
return N / (k - i);
}
int val_to_pos(ll l) {
if (l <= s) return l - 1;
return k - N / l;
}
V<mint> invert(const V<mint> &f, const V<mint> &h) {
// return g s.t. f * g = h
V<mint> g(k);
mint inv_f0 = mint(1) / f.front();
for (int i = 0; i < k; ++i) {
mint remainder = h.at(i);
if (i > 0) {
ll v = pos_to_val(i);
ll j = 1;
for (; j * j <= v; ++j) {
int p = val_to_pos(v / j);
if (j > 1) remainder -= (f[j - 1] - (j == 1 ? 0 : f[j - 2])) * g[p];
remainder -= (g[j - 1] - (j == 1 ? 0 : g[j - 2])) * f[p];
}
--j;
assert(j > 0);
remainder += f[j - 1] * g[j - 1];
}
g.at(i) = remainder * inv_f0;
}
return g;
}
};
// EndCodeSnip
void solve() {
ll N;
cin >> N;
Dirichlet dc(N);
int k = dc.k;
V<mint> f(k);
for (auto &t : f) {
int v;
cin >> v;
t = v;
}
vector<mint> h(k, 1);
auto ret = dc.invert(f, h);
for (int i = 0; i < k; ++i) { cout << ret.at(i).val() << " \n"[i + 1 == k]; }
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) solve();
}Ejemplo - Suma de totientes
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| YS | Sum of Totient Function | Normal | en el módulo |
Solución
Explicación
Sabemos que . Las sumas de prefijos de e son conocidas, así que todo lo que necesitamos hacer es aplicar la solución de “Inverso de Dirichlet y sumas de prefijos” con y .
Implementación
Código
Complejidad temporal:
#include <atcoder/modint>
#include <bits/stdc++.h>
using namespace std;
using namespace atcoder;
using mint = modint998244353;
template <class T> using V = vector<T>;
#define all(x) begin(x), end(x)
using ll = long long;
ll sq(ll x) { return x * x; }
// BeginCodeSnip{Dirichlet Inverse}
struct Dirichlet {
ll N;
int s = 0, k;
Dirichlet(ll N_) : N(N_) {
while (sq(s + 1) <= N) ++s;
k = 2 * s - (s == N / s);
}
ll pos_to_val(int i) {
if (i < s) return i + 1;
return N / (k - i);
}
int val_to_pos(ll l) {
if (l <= s) return l - 1;
return k - N / l;
}
V<mint> invert(const V<mint> &f, const V<mint> &h) {
// return g s.t. f * g = h
V<mint> g(k);
mint inv_f0 = mint(1) / f.front();
for (int i = 0; i < k; ++i) {
mint remainder = h.at(i);
if (i > 0) {
ll v = pos_to_val(i);
ll j = 1;
for (; j * j <= v; ++j) {
int p = val_to_pos(v / j);
if (j > 1) remainder -= (f[j - 1] - (j == 1 ? 0 : f[j - 2])) * g[p];
remainder -= (g[j - 1] - (j == 1 ? 0 : g[j - 2])) * f[p];
}
--j;
assert(j > 0);
remainder += f[j - 1] * g[j - 1];
}
g.at(i) = remainder * inv_f0;
}
return g;
}
};
// EndCodeSnip
void solve() {
ll N;
cin >> N;
Dirichlet dc(N);
int k = dc.k;
V<mint> f(k), h(k);
mint i2 = mint(1) / 2;
for (int i = 0; i < k; ++i) {
mint v = dc.pos_to_val(i);
f.at(i) = v;
h.at(i) = v * (v + 1) * i2;
}
auto ret = dc.invert(f, h);
cout << ret.back().val() << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
}Bonus: se puede optimizar esto a aplicando el método introducido en el segundo recurso. Más específicamente, y sus sumas de prefijos se pueden precomputar hasta en usando una criba lineal. Luego calcular las sumas de prefijos restantes toma de tiempo adicional.
Problemas
Los primeros dos problemas son del primer recurso.
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| HDU | Function | Normal | Solución | ||
| SPOJ | Counting Divisors (square) | Difícil | Solución | ||
| YS | Counting Square-free Integers | Difícil | Solución |