Definition
The Powerful Number (hereinafter PN) sieve is similar to the Du sieve, or more precisely an extension of it, and can be used to compute prefix sums of some multiplicative functions.
Requirement:
- There exists a function satisfying:
- is a multiplicative function.
- has easily computed prefix sums.
- For every prime , .
Assume that we want the prefix sum of the multiplicative function , .
Powerful Number
Definition: For a positive integer , write the prime factorization of as . is a PN if and only if .
Property 1: Every PN can be represented in the form .
Proof: If is even, merge into ; if is odd, first merge into , and then merge into .
Property 2: There are at most PNs no greater than .
Proof: Enumerate , then count the number of satisfying the condition. The number of PNs is approximately
So how do we find all PNs no greater than ? Use a linear sieve to find all primes no greater than , then search the exponents of the primes with DFS. Since there are at most PNs no greater than , we need search at most times.
PN Sieve
First, construct a multiplicative function whose prefix sums are easy to compute and which satisfies for every prime . Let .
Next, construct , where denotes Dirichlet-convolution division. From the properties of Dirichlet convolution, is also multiplicative, so . We have , where denotes Dirichlet convolution.
For a prime , . From and the fact that is multiplicative, we can conclude that for every non-PN number ; that is, is nonzero only at PNs.
Now, from , we have
Find all PNs in time and compute all effective values of . To compute all effective values of , it is enough to compute the values at all ; the multiplicativity of then gives all the effective values of . For every effective value , compute and add them to obtain .
Now consider computing . There are two methods: one is to derive directly a formula for that depends only on , and then compute from the formula; the other uses , which gives . Rearranging yields , so we can enumerate primes and then exponents to solve for all .
Procedure
- Construct .
- Construct a method for quickly computing .
- Compute .
- Search for PNs, accumulating the answer along the way.
- Obtain the result.
For step 3, we can compute directly from a formula, use enumeration to precompute a table, or derive the value temporarily when it is reached during the search.
Properties
Take the second method for computing as an example. The analysis can be divided into computing and searching.
For the first part, the number of primes no greater than is , and the exponent of each prime is at most . Computing requires iterations, so the time complexity of the first part is . This is a loose upper bound; depending on the problem, different optimizations can further reduce the time complexity of the first part.
For the search part, there are at most PNs no greater than , so we search at most times. For each PN, the time complexity also depends on how is computed. For example, if computing takes time, then the second part has complexity .
In particular, if the Du sieve is used to compute , then the time complexity of the second part is that of the Du sieve, namely . If we compute once in advance, optimize with a linear sieve, and record larger values in a data structure supporting fast random access (such as std::map and std::unordered_map in C++), then every used by the Du sieve is either recorded by the linear sieve or stored in std::map; this can be verified directly by a program.
For space complexity, the bottleneck is storing . If a two-dimensional array is used, where stores the value of , then the space complexity is .
Examples
Luogu P5325 【Template】 Min_25 Sieve
Problem: Given the multiplicative function , find .
It is easy to obtain , so construct .
Consider using the Du sieve to compute . From , we obtain .
The values of can then be computed by enumeration; this method is omitted.
In addition, this problem allows us to derive a formula for that depends only on . The derivation is as follows:
Using and summing, we obtain .
Reference code
#include <iostream>
#include <map>
using namespace std;
constexpr int MOD = 1e9 + 7;
template <typename T>
int mint(T x) {
x %= MOD;
if (x < 0) x += MOD;
return x;
}
int add(int x, int y) { return x + y >= MOD ? x + y - MOD : x + y; }
int mul(int x, int y) { return (long long)1 * x * y % MOD; }
int sub(int x, int y) {
return x < y ? x - y + MOD : x - y; // prevents negative values
}
int qp(int x, int y) {
int r = 1;
for (; y; y >>= 1) {
if (y & 1) r = mul(r, x);
x = mul(x, x);
}
return r;
}
int inv(int x) { return qp(x, MOD - 2); }
namespace PNS {
constexpr int N = 2e6 + 5;
constexpr int M = 35;
long long global_n;
int g[N], sg[N];
int h[N][M];
bool vis_h[N][M];
int ans;
int pcnt, prime[N], phi[N];
bool isp[N];
void sieve(int n) {
pcnt = 0;
for (int i = 2; i <= n; ++i) isp[i] = true; // mark the prime array
phi[1] = 1;
for (int i = 2; i <= n; ++i) {
if (isp[i]) {
++pcnt;
prime[pcnt] = i;
phi[i] = i - 1;
}
for (int j = 1; j <= pcnt; ++j) { // sieve out composite numbers
long long nxt = (long long)1 * i * prime[j];
if (nxt > n) break;
isp[nxt] = false;
if (i % prime[j] == 0) { // the case where i is composite
phi[nxt] = phi[i] * prime[j];
break;
}
phi[nxt] = phi[i] * phi[prime[j]];
}
}
for (int i = 1; i <= n; ++i) g[i] = mul(i, phi[i]);
sg[0] = 0;
for (int i = 1; i <= n; ++i) sg[i] = add(sg[i - 1], g[i]); // prefix sum of g
}
int inv2, inv6;
void init() {
sieve(N - 1);
for (int i = 1; i <= pcnt; ++i) h[i][0] = 1, h[i][1] = 0;
for (int i = 1; i <= pcnt; ++i) vis_h[i][0] = vis_h[i][1] = true;
inv2 = inv(2);
inv6 = inv(6);
}
int S1(long long n) { return mul(mul(mint(n), mint(n + 1)), inv2); }
int S2(long long n) {
return mul(mul(mint(n), mul(mint(n + 1), mint(n * 2 + 1))), inv6);
}
map<long long, int> mp_g;
int G(long long n) {
if (n < N) return sg[n];
if (mp_g.count(n)) return mp_g[n];
int ret = S2(n);
for (long long i = 2, j; i <= n; i = j + 1) {
j = n / (n / i);
ret = sub(ret, mul(sub(S1(j), S1(i - 1)), G(n / i)));
}
mp_g[n] = ret;
return ret;
}
void dfs(long long d, int hd, int pid) {
ans = add(ans, mul(hd, G(global_n / d)));
for (int i = pid; i <= pcnt; ++i) {
if (i > 1 && d > global_n / prime[i] / prime[i]) break; // pruning
int c = 2;
for (long long x = d * prime[i] * prime[i]; x <= global_n;
x *= prime[i], ++c) { // compute the f*g function
if (!vis_h[i][c]) {
int f = qp(prime[i], c);
f = mul(f, sub(f, 1));
int g = mul(prime[i], prime[i] - 1);
int t = mul(prime[i], prime[i]);
for (int j = 1; j <= c; ++j) {
f = sub(f, mul(g, h[i][c - j]));
g = mul(g, t);
}
h[i][c] = f;
vis_h[i][c] = true;
}
if (h[i][c]) dfs(x, mul(hd, h[i][c]), i + 1);
}
}
}
int solve(long long n) {
global_n = n;
ans = 0;
dfs(1, 1, 1);
return ans;
}
} // namespace PNS
int main() {
PNS::init();
long long n;
cin >> n;
cout << PNS::solve(n) << '\n';
return 0;
}
「LOJ #6053」Simple Function
Given :
It is easy to obtain:
Construct as
It is easy to prove that and is multiplicative.
Now consider computing .
Let and . Then .
When , we have
When , we have
Therefore, .
can be computed with the Du sieve, while follows directly from the formula, so can also be computed.
Reference code
#include <iostream>
#include <map>
using namespace std;
constexpr int MOD = 1e9 + 7;
constexpr int inv2 = (MOD + 1) / 2;
template <typename T>
int mint(T x) {
x %= MOD;
if (x < 0) x += MOD;
return x;
}
int add(int x, int y) {
return x + y >= MOD ? x + y - MOD : x + y; // prevents values from exceeding the modulus
}
int mul(int x, int y) { return (long long)1 * x * y % MOD; }
int sub(int x, int y) {
return x < y ? x - y + MOD : x - y; // prevents negative values
}
namespace PNS {
constexpr int N = 2e6 + 5;
constexpr int M = 35;
long long global_n;
int s1[N], s2[N];
int h[N][M];
bool vis_h[N][M];
int ans;
int pcnt, prime[N], phi[N];
bool isp[N];
void sieve(int n) {
pcnt = 0;
for (int i = 2; i <= n; ++i) isp[i] = true; // mark the prime array
phi[1] = 1;
for (int i = 2; i <= n; ++i) {
if (isp[i]) {
++pcnt;
prime[pcnt] = i;
phi[i] = i - 1;
}
for (int j = 1; j <= pcnt; ++j) { // sieve out composite numbers
long long nxt = (long long)1 * i * prime[j];
if (nxt > n) break;
isp[nxt] = false;
if (i % prime[j] == 0) { // the case where i is composite
phi[nxt] = phi[i] * prime[j];
break;
}
phi[nxt] = phi[i] * phi[prime[j]];
}
}
s1[0] = 0;
for (int i = 1; i <= n; ++i) s1[i] = add(s1[i - 1], phi[i]);
s2[0] = 0;
for (int i = 1; i <= n / 2; ++i) {
s2[i] = add(s2[i - 1], phi[2 * i]);
}
}
void init() {
sieve(N - 1);
for (int i = 1; i <= pcnt; ++i) h[i][0] = 1;
for (int i = 1; i <= pcnt; ++i) vis_h[i][0] = true;
}
map<long long, int> mp_s1;
int S1(long long n) {
if (n < N) return s1[n];
if (mp_s1.count(n)) return mp_s1[n];
int ret = mul(mul(mint(n), mint(n + 1)), inv2);
for (long long i = 2, j; i <= n; i = j + 1) {
j = n / (n / i);
ret = sub(ret, mul(mint(j - i + 1), S1(n / i)));
}
mp_s1[n] = ret;
return ret;
}
map<long long, int> mp_s2;
int S2(long long n) {
if (n < N / 2) return s2[n];
if (mp_s2.count(n)) return mp_s2[n];
int ret = add(S1(n), S2(n / 2));
mp_s2[n] = ret;
return ret;
}
int G(long long n) { return add(S1(n), mul(2, S2(n / 2))); }
void dfs(long long d, int hd, int pid) {
ans = add(ans, mul(hd, G(global_n / d)));
for (int i = pid; i <= pcnt; ++i) {
if (i > 1 && d > global_n / prime[i] / prime[i]) break; // pruning
int c = 2;
for (long long x = d * prime[i] * prime[i]; x <= global_n;
x *= prime[i], ++c) {
if (!vis_h[i][c]) {
int f = prime[i] ^ c, g = prime[i] - 1;
// handle p = 2 as a special case
if (i == 1) g = mul(g, 3);
for (int j = 1; j <= c; ++j) {
f = sub(f, mul(g, h[i][c - j]));
g = mul(g, prime[i]);
}
h[i][c] = f;
vis_h[i][c] = true;
}
if (h[i][c]) dfs(x, mul(hd, h[i][c]), i + 1);
}
}
}
int solve(long long n) {
global_n = n;
ans = 0;
dfs(1, 1, 1);
return ans;
}
} // namespace PNS
int main() {
PNS::init(); // preprocessing function
long long n;
cin >> n;
cout << PNS::solve(n) << '\n';
return 0;
}