--- tags: number-theory, fast-prime-counting, dp, sieve, sublinear --- # Fast prime counting Sea $\pi_0(n)$ el número de primos menores o iguales a $n$. Hallar $\pi_0(n)$ de forma eficiente para $n \leq 10^{12}$. Por el teorema de los números primos, una aproximaxión a $\pi_0(n)$ es $O \left( \dfrac{n}{\ln n} \right)$. Una forma de hacerlo de forma exacta es usando la criba lineal para hallar todos los primos hasta $n$, y al final devolver el tamaño del arreglo de primos. La complejidad sería $O(n)$, tanto como en tiempo y en memoria. ## Modificación de la criba de Eratostenes Sea $dp(k, n)$ la cantidad de números que quedan sin tachar en la criba hasta $n$ luego de haber procesado los primeros $k$ primos. Tenemos el siguiente caso base: $dp(0, n) = n-1$ para toda $n$. Para obtener la recurrencia de $dp(k, n)$, donde $k \geq 1$, veamos lo siguiente: - Supongamos que estamos procesando el $k$-ésimo primo, digamos que es $p_k$. Vemos que los números que estaban sin tachar en el paso anterior entre $p_k$ y $\lfloor n / p_k \rfloor$, los podemos multiplicar por $p_k$ y obtener nuevos taches en la criba. Es decir, el número de taches nuevos en la criba será: $dp(k-1, \lfloor n / p_k \rfloor) - dp(k-1, p_k - 1)$. - Por lo tanto, $dp(k, n) = dp(k-1, n) - [dp(k-1, \lfloor n / p_k \rfloor) - dp(k-1, p_k - 1)]$ para $n \geq p_k^2$, y $dp(k, n) = dp(k-1, n)$ para $n < p_k^2$. - La respuesta final, es decir, el valor de $\pi_0(n)$ lo hallamos en $dp(\pi_0(\lfloor \sqrt{n} \rfloor), n)$. - Sea $m = \lfloor \sqrt{n} \rfloor$. Vemos que para calcular $\pi_0(n)$ solo se visitan los estados de la forma $dp(k, x)$, donde $x = 1, 2, \ldots, m, \lfloor n/1 \rfloor, \lfloor n/2 \rfloor, \ldots, \lfloor n/m \rfloor$. Es decir, a lo más visitará $\pi_0(m) \cdot 2m \approx \dfrac{m}{\ln m} 2m \approx \dfrac{4n}{\ln n}$ estados distintos. Ejemplo para $n=101$: ![](https://i.imgur.com/afz0K1i.png) ![](https://i.imgur.com/1H4gPNO.png) La implementación de esa idea, optimizando la dp de forma iterativa y ahorrando memoria, queda como: ```c++ lli pi_0(lli N){ int m = sqrt(N); vector<lli> lo(m+1), hi(m+1); auto at = [&](lli x) -> lli&{ if(x <= m) return lo[x]; else return hi[N / x]; }; vector<lli> values; for(int i = 1; i <= m; ++i){ values.push_back(i); if(N / i != i) values.push_back(N / i); } sort(values.begin(), values.end(), greater<lli>()); for(lli n : values){ at(n) = n - 1; } for(lli p = 2; p <= m; ++p){ if(at(p) == at(p - 1)) continue; for(lli n : values){ if(n < p*p) break; at(n) = at(n) - (at(n / p) - at(p - 1)); } } return at(N); } ``` La complejidad en memoria de esta idea es $O(\sqrt{n})$ y en tiempo es $O\left( \dfrac{n^{3/4}}{\ln(n)} \right)$. ## Generalización Sea $\pi_q(n)$ la suma de las $q$-ésimas potencias de los primos menores o iguales a $n$. Para hallar $\pi_q(n)$, tenemos que modificar la definición de $dp(k, n)$ para hallar la suma de las $q$-ésimas potencias de los números hasta $n$ que están sin tachar luego de haber procesado los primeros $k$ primos. - El caso base sería $dp(0, n) =\displaystyle \sum_{i=2}^{n} i^q$, el cual puede ser calculado con una fórmula sencilla para valores pequeños de $q$. - La recurrencia quedaría como: $dp(k, n) = dp(k-1, n) - p_k^q [dp(k-1, \lfloor n / p_k \rfloor) - dp(k-1, p_k - 1)]$ para $n \geq p_k^2$, y $dp(k, n) = dp(k-1, n)$ para $n < p_k^2$.