Min_25 筛
tags: 数学 数论
简介
Min_25 筛法用于求解一些积性函数的前缀和.
具体来说,对于积性函数的前缀和函数 (S),Min_25 筛可以求出 (S(n)),并同时求出 (S(lfloorfrac{n}{2}
floor),S(lfloorfrac{n}{3}
floor),cdots,S(1)).
适用条件
设要求的函数为 (F).
- 该函数为积性函数(个别非积性函数也可以);
- 若 (p) 为质数,(F(p^e)) 可以表示为一个可以快速求得的多项式.
记号
(mathbb{P}) 表示质数集
( ext{lpf}(n)) 表示 (n) 的最小质因数
(p_i) 表示从小到大第 (i) 个质数(定义(p_0 = 1))
(S_j(n) = sumlimits_{i = 1, ext{lpf}(i)>p_j}^{n}F(i))
(S_{ ext{pri}}(n)=sumlimits_{i=2,iin mathbb{P}}^{n}F(i))
求解方法
第一部分
我们发现答案 (sumlimits_{i=1}^n F(i)=S_0(n)+F(1))
该式可以递归求解(下文 P5325 代码 1),也可以通过 (S_{j}(i)) 转移出 (S_{j-1}(i)) (P5325 代码 2)
式子的第一部分( (sumlimits_{e=1}^{p_j^ele i}F(p_j^e)) )可以快速地求和(暴力枚举 (e)).
现在考虑第二部分:
令
由于对于 (forall x=p^e,pinmathbb{P}),有 (F(x)) 是多项式(不妨设 (F(x)=a_0+a_1x^1+a_2x^2+cdots+a_mx^m)),那么我们把 (T_j(i)) 中的 (F(p_j^e)) 拆开:
设
有
(我们只要求出每个 (T_{j,k}),就可以求和得到 (T_j))
对于 (T_k) 考虑从 (T_k(lfloor frac{n}{p_j} floor)) 转移. 具体而言:
当 (p_{j+1}>sqrt n) 时,对 (S_j) 产生贡献的所有 (F(x)) 满足 (x in mathbb{P}).
因此,此时的
所以我们可以从满足 (p_jle sqrt{n}) 的最大的 (j) 算起,通过转移式一直转移到 (j=0),从而求出 (S_0(n)).
时间复杂度:
质数密度估计为 (O(frac{n}{ln{n}}))
由于 (lfloorfrac{n}{i}
floor) 只有 (O(sqrt n)) 个值,时间复杂度为 (O(sumlimits_{i=1}^{sqrt{n}}frac{sqrt{i}}{lnsqrt{i}}+sumlimits_{i=1}^{sqrt{n}}frac{sqrt{lfloorfrac{n}{i}
floor}}{lnsqrt{lfloorfrac{n}{i}
floor}})=O(frac{n^{frac{3}{4}}}{log n}))
第二部分
第一部分中,求 (S_j(i)) 时用到了 (S_{ ext{pri}}(i)) 和 (S_{ ext{pri}}(p_j)),其中 (i) 的取值为 (n, lfloorfrac{n}{2} floor,lfloorfrac{n}{3} floor,cdots,1),(j) 满足 (p_jlesqrt{n}).
后者可以通过线性筛筛出 (sqrt{n}) 内的质数算出,而前者的计算方法如下.
首先把 (F(mathbb{P})) 拆成形如 (mathbb{P}^k) 的若干项,对每一项分别求解.
(S_{ ext{pri}}(i)=sumlimits_{x=1,xinmathbb{P}}^{i}F(i)=sumlimits_{x=1,xinmathbb{P}}^{i}x^k)
设 (S'_j(i)=sumlimits_{x=1,xin mathbb{P}operatorname{or} ext{lpf}(i)>p_j}^{i}x^k),即埃氏筛法中筛完第 (j) 轮后 (1...n) 中剩下的数的函数值之和.
同样的,我们尝试从 (S'_{j-1}(i)) 转移到 (S'_j(i)).
(S'_0(i)=sumlimits_{x=2}^{i}x^k),可以快速求得.
根据埃氏筛法可知,若 (j) 取满足 (p_jlesqrt{i}) 时的最大值,(S_{ ext{pri}}(i)=S'_{j}(i)).
对于 (i),发现有用的取值也是 (n, lfloorfrac{n}{2} floor,lfloorfrac{n}{3} floor,cdots,1).
我们把 (j) 从 (0) 一直转移到满足 (p_j^2le n) 时的最大值,即可求出 (i) 取 (n, lfloorfrac{n}{2} floor,lfloorfrac{n}{3} floor,cdots,1) 时的所有 (S_{ ext{pri}}(i)).
时间复杂度与优化后的第一部分是一样的.
总结
该算法分3步完成:
- 筛出 (sqrt{n}) 之内的素数并求它们的函数值的前缀和;
- 求出 (S_{ ext{pri}}) 数组(第二部分);
- 求出 (S) 数组(第一部分).
其中第一步时间复杂度 (O(sqrt{n})),第二部和第三部均为 (O(frac{n^{frac{3}{4}}}{log n})),所以总时间复杂度为 (O(frac{n^{frac{3}{4}}}{log n})).
例题
以洛谷模板题 P5325 为例.
由题知,(F(p^e) = (p^e)^2-p^e),即可拆为两项处理.
代码如下:
/*
本代码用DP转移的方式计算S,常数较大,但能在求出S(n)的同时求出S(n/2),S(n/3),...,S(1)
*/
#include <bits/stdc++.h>
using namespace std;
const long long mod = 1000000007;
const int maxn = 200000;
long long n;
int rt;
int pri[maxn + 5];
bool vis[maxn + 5];
int pri_cnt;
long long prisum1[maxn + 5], prisum2[maxn + 5];
long long g1[maxn + 5], g2[maxn + 5];
long long t1[maxn + 5], t2[maxn + 5];
long long l[maxn + 5];
long long s[maxn + 5];
int id1[maxn + 5], id2[maxn + 5];
long long num[maxn + 5];
void sieve(int l) {
for (int i = 2; i <= l; i++) {
if (!vis[i])
pri[++pri_cnt] = i;
for (int j = 1; i * pri[j] <= l; j++) {
vis[i * pri[j]] = true;
if (i % pri[j] == 0)
break;
}
}
}
long long find(long long t) {
return t = (t <= rt) ? id1[t] : id2[n / t];
}
int cnt = 1;
void calcg() { // 计算Spri
for (int i = 1; i <= pri_cnt; i++) {
prisum1[i] = (prisum1[i - 1] + pri[i]) % mod;
prisum2[i] = (prisum2[i - 1] + ((long long)pri[i] * pri[i]) % mod) % mod;
}
for (long long l = 1, r; l <= n; l = r + 1, cnt++) {
r = n / (n / l);
num[cnt] = n / l;
long long tmp = num[cnt] % mod;
g1[cnt] = (((tmp + 1) * tmp / 2ll) % mod + mod - 1) % mod;
g2[cnt] = (((((tmp + 1) * tmp % mod) * (2ll * tmp + 1)) % mod) * 166666668 + mod - 1) % mod;
if (num[cnt] <= rt)
id1[num[cnt]] = cnt;
else
id2[l] = cnt;
}
for (int j = 1; j <= pri_cnt; j++) {
for (int i = 1; i <= cnt && (long long)pri[j] * pri[j] <= num[i]; i++) {
long long t = find(num[i] / pri[j]);
g1[i] = (g1[i] - pri[j] * (g1[t] - prisum1[j - 1] + mod) % mod + mod) % mod;
g2[i] = (g2[i] - ((long long)pri[j] * pri[j] % mod) * (g2[t] - prisum2[j - 1] + mod) % mod + mod) % mod;
}
}
}
void calcs() { // 计算S
memset(s, -1, sizeof(s));
for (int j = pri_cnt; j > 0; j--) {
int top = 1;
while (top <= cnt && (long long)pri[j] * pri[j] <= num[top])
top++;
top--;
long long p = pri[j];
l[top + 1] = 0;
for (int i = top; i > 0; i--) {
if (s[i] == -1)
s[i] = (g2[i] - prisum2[j] - g1[i] + prisum1[j] + 2ll * mod) % mod;
l[i] = l[i + 1];
while (num[i] >= p) {
l[i] = (l[i] + (long long)(p % mod) * (p % mod) % mod - p + mod) % mod;
p *= pri[j];
}
long long t = find(num[i] / pri[j]);
if (num[i] / pri[j] < (long long)pri[j] * pri[j]) {
t1[i] = (long long)pri[j] * (g2[t] - prisum2[j] - g1[t] + prisum1[j] + 2ll * mod) % mod;
t2[i] = ((long long)pri[j] * pri[j] % mod) * (g2[t] - prisum2[j] - g1[t] + prisum1[j] + 2ll * mod) % mod;
} else {
t1[i] = (long long)pri[j] * (t1[t] + s[t]) % mod;
t2[i] = ((long long)pri[j] * pri[j] % mod) * (t2[t] + s[t]) % mod;
}
}
for (int i = 1; i <= top; i++)
s[i] = (s[i] + l[i] + t2[i] - t1[i] + mod) % mod;
}
}
int main() {
scanf("%lld", &n);
rt = sqrt(n);
sieve(rt);
calcg();
calcs();
printf("%lld", max(0ll, s[find(n)]) + 1);
}
/*
本代码用递归的方式计算S,常数较小
*/
#include <bits/stdc++.h>
using namespace std;
const long long mod = 1000000007;
const int maxn = 200000;
long long n;
int rt;
int pri[maxn + 5];
bool vis[maxn + 5];
int pri_cnt;
long long prisum1[maxn + 5], prisum2[maxn + 5];
long long g1[maxn + 5], g2[maxn + 5];
int id1[maxn + 5], id2[maxn + 5];
long long num[maxn + 5];
void sieve(int l) {
for (int i = 2; i <= l; i++) {
if (!vis[i])
pri[++pri_cnt] = i;
for (int j = 1; i * pri[j] <= l; j++) {
vis[i * pri[j]] = true;
if (i % pri[j] == 0)
break;
}
}
}
void calcg() {
for (int i = 1; i <= pri_cnt; i++) {
prisum1[i] = (prisum1[i - 1] + pri[i]) % mod;
prisum2[i] = (prisum2[i - 1] + ((long long)pri[i] * pri[i]) % mod) % mod;
}
int cnt = 1;
for (long long l = 1, r; l <= n; l = r + 1, cnt++) {
r = n / (n / l);
num[cnt] = n / l;
long long tmp = num[cnt] % mod;
g1[cnt] = (((tmp + 1) * tmp / 2ll) % mod + mod - 1) % mod;
g2[cnt] = (((((tmp + 1) * tmp % mod) * (2ll * tmp + 1)) % mod) * 166666668 + mod - 1) % mod;
if (num[cnt] <= rt)
id1[num[cnt]] = cnt;
else
id2[l] = cnt;
}
for (int j = 1; j <= pri_cnt; j++) {
for (int i = 1; i <= cnt && (long long)pri[j] * pri[j] <= num[i]; i++) {
long long t = num[i] / pri[j];
t = (t <= rt) ? id1[t] : id2[n / t];
g1[i] = (g1[i] - pri[j] * (g1[t] - prisum1[j - 1] + mod) % mod + mod) % mod;
g2[i] = (g2[i] - ((long long)pri[j] * pri[j] % mod) * (g2[t] - prisum2[j - 1] + mod) % mod + mod) % mod;
}
}
}
long long f(long long x, int y) {
if (pri[y] >= x)
return 0;
int t = (x <= rt) ? id1[x] : id2[n / x];
long long res = (g2[t] - prisum2[y] - g1[t] + prisum1[y] + 2ll * mod) % mod;
for (int i = y + 1; i <= pri_cnt && (long long)pri[i] * pri[i] <= x; i++) {
long long num = pri[i];
for (int j = 1; num <= x; j++, num *= pri[i]) {
long long tmp = num % mod;
res = (res + (tmp * (tmp - 1 + mod) % mod) * (f(x / num, i) + (int)(j > 1)) % mod) % mod;
}
}
return res;
}
int main() {
scanf("%lld", &n);
rt = sqrt(n);
sieve(rt);
calcg();
printf("%lld", f(n, 0) + 1);
}