从此种筛法的思想方法来说,其又被称为「Extended Eratosthenes Sieve」。
由于其由 Min_25 发明并最早开始使用,故称「Min_25 筛」。
其可以在 O(lognn43) 或 Θ(n1−ϵ) 的时间复杂度下解决一类 积性函数 的前缀和问题。
要求:f(p) 是一个关于 p 可以快速求值的完全积性函数之和(例如多项式);f(pc) 可以快速求值。
- 如无特别说明,本节中所有记为 p 的变量的取值集合均为全体质数。
- x/y:=⌊yx⌋
- isprime(n):=[∣{d:d∣n}∣=2],即 n 为质数时其值为 1,否则为 0。
- pk:全体质数中第 k 小的质数(如:p1=2,p2=3)。特别地,令 p0=1。
- lpf(n):=[1<n]min{p:p∣n}+[1=n],即 n 的最小质因数。特别地,n=1 时,其值为 1。
- Fprime(n):=∑2≤p≤nf(p)
- Fk(n):=∑i=2n[pk≤lpf(i)]f(i)
观察 Fk(n) 的定义,可以发现答案即为 F1(n)+f(1)=F1(n)+1。
考虑如何求出 Fk(n)。通过枚举每个 i 的最小质因子及其次数可以得到递推式:
Fk(n)=i=2∑n[pk≤lpf(i)]f(i)=k≤ipi2≤n∑c≥1pic≤n∑f(pic)([c>1]+Fi+1(n/pic))+k≤ipi≤n∑f(pi)=k≤ipi2≤n∑c≥1pic≤n∑f(pic)([c>1]+Fi+1(n/pic))+Fprime(n)−Fprime(pk−1)=k≤ipi2≤n∑c≥1pic+1≤n∑(f(pic)Fi+1(n/pic)+f(pic+1))+Fprime(n)−Fprime(pk−1)
最后一步推导基于这样一个事实:对于满足 pic≤n<pic+1 的 c,有 pic+1>n⟺n/pic<pi<pi+1,故 Fi+1(n/pic)=0。
其边界值即为 Fk(n)=0(pk>n)。
假设现在已经求出了所有的 Fprime(n),那么有两种方式可以求出所有的 Fk(n):
- 直接按照递推式计算。
- 从大到小枚举 p 转移,仅当 p2<n 时转移增加值不为零,故按照递推式后缀和优化即可。
现在考虑如何计算 Fprime(n)。
观察求 Fk(n) 的过程,容易发现 Fprime 有且仅有 1,2,…,⌊n⌋,n/n,…,n/2,n 这 O(n) 处的点值是有用的。
一般情况下,f(p) 是一个关于 p 的低次多项式,可以表示为 f(p)=∑aipci。
那么对于每个 pci,其对 Fprime(n) 的贡献即为 ai∑2≤p≤npci。
分开考虑每个 pci 的贡献,问题就转变为了:给定 n,s,g(p)=ps,对所有的 m=n/i,求 ∑p≤mg(p)。
Notice:g(p)=ps 是完全积性函数!
于是设 Gk(n):=∑i=2n[pk<lpf(i)∨isprime(i)]g(i),即埃筛第 k 轮筛完后剩下的数的 g 值之和。
对于一个合数 x≤n,必定有 lpf(x)≤x≤n。设 pℓ(n) 为不大于 n 的最大质数,则 Fprime(n)=Gℓ(n)(n),即在埃筛进行 ℓ 轮之后剩下的均为质数。
考虑 G 的边界值,显然为 G0(n)=∑i=2ng(i)。(还记得吗?特别约定了 p0=1)
对于转移,考虑埃筛的过程,分开讨论每部分的贡献,有:
- 对于 n<pk2 的部分,G 值不变,即 Gk(n)=Gk−1(n)。
- 对于 pk2≤n 的部分,被筛掉的数必有质因子 pk,即 −g(pk)Gk−1(n/pk)。
- 对于第二部分,由于 pk2≤n⟺pk≤n/pk,满足 lpf(i)<pk 的 i 会被额外减去。这部分应当加回来,即 g(pk)Gk−1(pk−1)。
则有:
Gk(n)=Gk−1(n)−[pk2≤n]g(pk)(Gk−1(n/pk)−Gk−1(pk−1))
复杂度分析
对于 Fk(n) 的计算,其第一种方法的时间复杂度被证明为 O(n1−ϵ)(见 zzt 集训队论文 2.3);
对于第二种方法,其本质即为洲阁筛的第二部分,在洲阁论文中也有提及(6.5.4),其时间复杂度被证明为 O(lognn43)。
对于 Fprime(n) 的计算,事实上,其实现与洲阁筛第一部分是相同的。
考虑对于每个 m=n/i,只有在枚举满足 pk2≤m 的 pk 转移时会对时间复杂度产生贡献,则时间复杂度可估计为:
T(n)=i2≤n∑O(π(i))+i2≤n∑O(π(in))=i2≤n∑O(lnii)+i2≤n∑O(lninin)=O(∫1nlogxnxndx)=O(lognn43)
对于空间复杂度,可以发现不论是 Fk 还是 Fprime,其均只在 n/i 处取有效点值,共 O(n) 个,仅记录有效值即可将空间复杂度优化至 O(n)。
首先,通过一次数论分块可以得到所有的有效值,用一个大小为 O(n) 的数组 lis 记录。对于有效值 v,记 id(v) 为 v 在 lis 中的下标,易得:对于所有有效值 v,id(v)≤n。
然后分开考虑小于等于 n 的有效值和大于 n 的有效值:对于小于等于 n 的有效值 v,用一个数组 le 记录其 id(v),即 lev=id(v);对于大于 n 的有效值 v,用一个数组 ge 记录 id(v),由于 v 过大所以借助 v′=n/v<n 记录 id(v),即 gev′=id(v)。
这样,就可以使用两个大小为 O(n) 的数组记录所有有效值的 id 并 O(1) 查询。在计算 Fk 或 Fprime 时,使用有效值的 id 代替有效值作为下标,即可将空间复杂度优化至 O(n)。
对于 Fk(n) 的计算,我们实现时一般选择实现难度较低的第一种方法,其在数据规模较小时往往比第二种方法的表现要好;
对于 Fprime(n) 的计算,直接按递推式实现即可。
对于 pk2≤n,可以用线性筛预处理出 sk:=Fprime(pk) 来替代 Fk 递推式中的 Fprime(pk−1)。
相应地,G 递推式中的 Gk−1(pk−1)=∑i=1k−1g(pi) 也可以用此方法预处理。
用 Extended Eratosthenes Sieve 求 积性函数 f 的前缀和时,应当明确以下几点:
- 如何快速(一般是线性时间复杂度)筛出前 n 个 f 值;
- f(p) 的多项式表示;
- 如何快速求出 f(pc)。
明确上述几点之后按顺序实现以下几部分即可:
- 筛出 [1,n] 内的质数与前 n 个 f 值;
- 对 f(p) 多项式表示中的每一项筛出对应的 G,合并得到 Fprime 的所有 O(n) 个有用点值;
- 按照 Fk 的递推式实现递归,求出 F1(n)。
求莫比乌斯函数的前缀和
求 i=1∑nμ(i)。
易知 f(p)=−1。则 g(p)=−1,G0(n)=∑i=2ng(i)=−n+1。
直接筛即可得到 Fprime 的所有 O(n) 个所需点值。
求欧拉函数的前缀和
求 i=1∑nφ(i)。
首先易知 f(p)=p−1。
对于 f(p) 的一次项 (p),有 g(p)=p,G0(n)=∑i=2ng(i)=2(n+2)(n−1);
对于 f(p) 的常数项 (−1),有 g(p)=−1,G0(n)=∑i=2ng(i)=−n+1。
筛两次加起来即可得到 Fprime 的所有 O(n) 个所需点值。
给定 f(n):
f(n)=⎩⎨⎧1pxorcf(a)f(b)n=1n=pcn=ab∧a⊥b
易知 f(p)=p−1+2[p=2]。则按照筛 φ 的方法筛,对 2 讨论一下即可。
此处给出一种 C++ 实现:
#include <cmath>
#include <iostream>
constexpr int MAXS = 200000;
constexpr int mod = 1000000007;
template <typename x_t, typename y_t>
void inc(x_t &x, const y_t &y) {
x += y;
(mod <= x) && (x -= mod);
}
template <typename x_t, typename y_t>
void dec(x_t &x, const y_t &y) {
x -= y;
(x < 0) && (x += mod);
}
template <typename x_t, typename y_t>
int sum(const x_t &x, const y_t &y) {
return x + y < mod ? x + y : (x + y - mod);
}
template <typename x_t, typename y_t>
int sub(const x_t &x, const y_t &y) {
return x < y ? x - y + mod : (x - y);
}
template <typename _Tp>
int div2(const _Tp &x) {
return ((x & 1) ? x + mod : x) >> 1;
}
template <typename _Tp>
long long sqrll(const _Tp &x) {
return (long long)x * x;
}
int pri[MAXS / 7], lpf[MAXS + 1], spri[MAXS + 1], pcnt;
void sieve(const int &n) {
for (int i = 2; i <= n; ++i) {
if (lpf[i] == 0) {
lpf[i] = ++pcnt;
pri[lpf[i]] = i;
spri[pcnt] = sum(spri[pcnt - 1], i);
}
for (int j = 1, v; j <= lpf[i] && (v = i * pri[j]) <= n; ++j) lpf[v] = j;
}
}
long long global_n;
int lim;
int le[MAXS + 1],
ge[MAXS + 1];
#define idx(v) (v <= lim ? le[v] : ge[global_n / v])
int G[MAXS + 1][2], Fprime[MAXS + 1];
long long lis[MAXS + 1];
int cnt;
void init(const long long &n) {
for (long long i = 1, j, v; i <= n; i = n / j + 1) {
j = n / i;
v = j % mod;
lis[++cnt] = j;
(j <= lim ? le[j] : ge[global_n / j]) = cnt;
G[cnt][0] = sub(v, 1ll);
G[cnt][1] = div2((long long)(v + 2ll) * (v - 1ll) % mod);
}
}
void calcFprime() {
for (int k = 1; k <= pcnt; ++k) {
const int p = pri[k];
const long long sqrp = sqrll(p);
for (int i = 1; lis[i] >= sqrp; ++i) {
const long long v = lis[i] / p;
const int id = idx(v);
dec(G[i][0], sub(G[id][0], k - 1));
dec(G[i][1], (long long)p * sub(G[id][1], spri[k - 1]) % mod);
}
}
for (int i = 1; i <= cnt; ++i) Fprime[i] = sub(G[i][1], G[i][0]);
}
int f_p(const int &p, const int &c) {
return p ^ c;
}
int F(const int &k, const long long &n) {
if (n < pri[k] || n <= 1) return 0;
const int id = idx(n);
long long ans = Fprime[id] - (spri[k - 1] - (k - 1));
if (k == 1) ans += 2;
for (int i = k; i <= pcnt && sqrll(pri[i]) <= n; ++i) {
long long pw = pri[i], pw2 = sqrll(pw);
for (int c = 1; pw2 <= n; ++c, pw = pw2, pw2 *= pri[i])
ans +=
((long long)f_p(pri[i], c) * F(i + 1, n / pw) + f_p(pri[i], c + 1)) %
mod;
}
return ans % mod;
}
using std::cin;
using std::cout;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> global_n;
lim = sqrt(global_n);
sieve(lim + 1000);
init(global_n);
calcFprime();
cout << (F(1, global_n) + 1ll + mod) % mod << '\n';
return 0;
}