专栏文章
Min_25 筛
算法·理论参与者 35已保存评论 39
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 39 条
- 当前快照
- 1 份
- 快照标识符
- @mhz5rzz4
- 此快照首次捕获于
- 2025/11/15 01:55 4 个月前
- 此快照最后确认于
- 2025/11/29 05:24 3 个月前
简介
对于积性函数 ,如果能快速知道 的值(其中 为质数,下同),且 可以表示为项数较少的多项式(或能表示成若干项使得每一项都是完全积性函数),那么 筛可以在 时间复杂度内求出 。
由于其思想与埃氏筛类似,所以也被称作扩展埃氏筛。
内容
第一步
推导
目标:求 。
不妨设 是完全积性函数,如果不是可以尝试拆成若干项完全积性函数,分别求然后相加。
首先要线性筛求出 以内的质数。
很难直接求解,考虑用 DP 计算。设 ,其中 表示第 个质数,那么我们要的就是 。考虑从 变到 ,那么最小质因子为 的合数会被筛掉,那么它们的贡献要减去。则有转移
系数 表示由于 是完全积性函数,所以可以把它从后面提出来。 表示考虑所有 的倍数,它们除以 之后,最小质因子 的合数以及所有质数的贡献,应当减去。但是,这些质数中可能有 的,它们在之前就被筛掉过了,所以要加回来,也就是 。
由于有公式 ,因此容易发现上述式子只会用到形如 的点处的 DP 值,即第一项的状态数是 (实际实现的时候注意状态数是 )。我们预处理出这 个数,把他们离散化,顺带求出 ,然后 DP 即可。
注意到上述转移式中还有一项 ,我们只处理了所有形如 的数,是否会漏掉某些质数 呢?其实不会漏。注意到,能用来转移的 一定满足 。我们只需要证明 。
证明:
- 若 ,设 ,则由于 ,故 。
- ,那么 。
- ,那么 。
- 若 即 ,假设存在 ,此时 恰好夹在两个连续的 之间,即不可被表出。则 ,故 ,从而 。另一方面,,所以 ,于是 ,因此 ,于是得到 ,故假设不成立,原命题成立。
所以,上述担心就是多虑了。
我看到很多博客、题解都额外预处理了 内所有素数处的函数值的前缀和,但因为我“粗心”,代码中直接调用 数组,却顺利通过了一堆题目,写笔记时意识到这一点,于是琢磨出了奥妙所在。其实,在看 zzt 集训队论文时注意到他说只需要用到 处的值,就心生疑惑,现在终于明白了。因此,在我看来,预处理根号内素数处的函数前缀和的值是不必要的。
时间复杂度
第二步
推导
目标:求 。与第一步类似,设 。但此处 不需要再拆分成单项式,直接是原函数即可(因为不需要依赖于完全积性,只需要积性即可)(但要能快速计算 的值)。
方法一
考虑把贡献拆成质数的和合数的,合数枚举最小质因子以及次数,于是有转移:
最后一项 的意思是,对于 的情况, 没有计算 贡献,刚好,因为此时 是质数,其贡献在之前计算过;对于 的情况, 是合数,贡献算漏了,要补上。直接暴力递归计算(并且不需要记忆化)。
方法二
也是把贡献拆成质数和合数,只是采用类似于第一步的递推方式:
但直接这样转移复杂度不对,我们要严格控制只有 的质数才发生转移。即,对于 的状态 ,不显式地计算出来,用到的时候特判为 。所以,我们要更新状态 时,一定有 ,此时需要用到 ,即使要特判为 ,由步骤一中的论述可知 已经计算出,所以不会有问题。需要注意 的状态不会被任何 更新到,所以也要特判。一种较为简洁的方法是,像方法一的代码一样写一个函数 ,只不过不用来递归,而是用来实现各种特判。
但是,这样代码不太好些,且实现出来常数比较大。我实现了一份:code。耗时几乎是别人的两倍。
方法二改进
既然采用了类似第一步的递推方式,干脆直接套用第一步的状态设计。设 ,那么参考第一步的方程,有转移:
最后面的 是因为, 中包含了 的质数的贡献,与转移式的意义不符,应当减去。但是 里面有个 非常令人不爽,并且为了卡常,我们可以进一步简化上式:
这是因为当 的时候 这一项不会产生贡献。(其实方法一也可以这样简化式子。)
所以只需要设置 初值为 ,即可进行和第一步几乎一样的 DP 过程了。
此外,注意这种方法不仅计算出了 ,也顺带计算出了所有 ,这是方法一做不到的。
时间复杂度
对于第一种方法,zzt 大佬在集训队论文中证明,一般情况下其复杂度将会是 ,其中 代表一个无穷小量。但是,他也证明了,在 时,复杂度是 !
对于第二种方法,其复杂度和第一步类似;虽然要枚举质数的指数,但随 的增大,指数的枚举次数下降速率急剧趋缓,因而不会对复杂度产生太大影响,视为常数(?) 听说其密度确实仍然是 级别。复杂度 。不过第一种方法常数更小,实际运行效可能会高那么一点点。
实现细节和代码
第一步中要使空间为 ,可以考虑根号分治表示下标。即,对于 ,用 映射到下标;对于 ,用 映射到下标。
第一步把原函数拆成 两个完全积性函数计算。
方法一
第二步中直接递归计算,但是用了化简过的递推式。
CPP#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5, mod = 1e9 + 7, i2 = 5e8 + 4, i6 = 166666668;
typedef long long ll;
ll n;
int id1[N], id2[N], m;
int p[N], vis[N], cnt;
ll g1[N], g2[N], v[N];
inline int get(ll x) { return x < N ? id1[x] : id2[n/x]; }
inline ll S1(ll x) { return x %= mod, x * (x + 1) % mod * i2 % mod; }
inline ll S2(ll x) { return x %= mod, x * (x + 1) % mod * (2 * x + 1) % mod * i6 % mod; }
inline ll sq(ll x) { return x %= mod, x * x % mod; }
inline ll F(ll x) { return x %= mod, (sq(x) - x + mod) % mod; }
inline void init(int n) {
for (int i = 2; i <= n; i++) {
if (!vis[i]) p[++cnt] = i;
for (int j = 1; j <= cnt && p[j] <= n / i; j++) {
vis[i*p[j]] = 1;
if (i % p[j] == 0) break;
}
}
}
ll S(ll x, int y) {
if (p[y] >= x) return 0;
ll res = (g2[get(x)] - g1[get(x)] - g2[get(p[y])] + g1[get(p[y])] + 2 * mod) % mod;
for (int i = y + 1; i <= cnt && p[i] <= x / p[i]; i++) {
ll w = p[i];
for (int j = 1; w <= x / p[i]; j++, w = w * p[i])//注意后面有 x / w, 所以此处不能取模
res = (res + F(w) * S(x / w, i) % mod + F(w * p[i])) % mod;
}
return res;
}
int main()
{
scanf("%lld", &n), init(sqrt(n) + 1);
for (ll l = 1, r; l <= n; l = r + 1) {
r = n / (n / l), v[++m] = n / l;
if (v[m] < N) id1[v[m]] = m;
else id2[n/v[m]] = m;
g1[m] = (S1(v[m]) - 1 + mod) % mod, g2[m] = (S2(v[m]) - 1 + mod) % mod;
}
for (int j = 1; j <= cnt; j++) {
for (int i = 1; i <= m && p[j] <= v[i] / p[j]; i++) {
g1[i] = (g1[i] - p[j] * (g1[get(v[i]/p[j])] - g1[get(p[j-1])]) % mod + mod) % mod;
g2[i] = (g2[i] - sq(p[j]) * (g2[get(v[i]/p[j])] - g2[get(p[j-1])]) % mod + mod) % mod;
}
}
printf("%lld\n", (S(n, 0) + 1) % mod);
return 0;
}
方法二
使用改进后的递推方式。
CPP#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5, mod = 1e9 + 7, i2 = 5e8 + 4, i6 = 166666668;
typedef long long ll;
ll n;
int id1[N], id2[N], m;
int p[N], vis[N], cnt;
ll s[N], g1[N], g2[N], v[N];
inline int get(ll x) { return x < N ? id1[x] : id2[n/x]; }
inline ll S1(ll x) { return x %= mod, x * (x + 1) % mod * i2 % mod; }
inline ll S2(ll x) { return x %= mod, x * (x + 1) % mod * (2 * x + 1) % mod * i6 % mod; }
inline ll sq(ll x) { return x %= mod, x * x % mod; }
inline ll F(ll x) { return x %= mod, (sq(x) - x + mod) % mod; }
inline void init(int n) {
for (int i = 2; i <= n; i++) {
if (!vis[i]) p[++cnt] = i;
for (int j = 1; j <= cnt && p[j] <= n / i; j++) {
vis[i*p[j]] = 1;
if (i % p[j] == 0) break;
}
}
}
int main()
{
scanf("%lld", &n), init(sqrt(n) + 1);
for (ll l = 1, r; l <= n; l = r + 1) {
r = n / (n / l), v[++m] = n / l;
if (v[m] < N) id1[v[m]] = m;
else id2[n/v[m]] = m;
g1[m] = (S1(v[m]) - 1 + mod) % mod, g2[m] = (S2(v[m]) - 1 + mod) % mod;
}
for (int j = 1; j <= cnt; j++) {
for (int i = 1; i <= m && p[j] <= v[i] / p[j]; i++) {
g1[i] = (g1[i] - p[j] * (g1[get(v[i]/p[j])] - g1[get(p[j-1])]) % mod + mod) % mod;
g2[i] = (g2[i] - sq(p[j]) * (g2[get(v[i]/p[j])] - g2[get(p[j-1])]) % mod + mod) % mod;
}
}
for (int i = 1; i <= m; i++) s[i] = g1[i] = (g2[i] - g1[i] + mod) % mod;
for (int j = cnt; j >= 1; j--) {
for (int i = 1; i <= m && p[j] <= v[i] / p[j]; i++) {
ll w = p[j];
for (int k = 1; w <= v[i] / p[j]; k++, w *= p[j])
s[i] = (s[i] + F(w) * (s[get(v[i]/w)] - g1[get(p[j])] + mod) % mod + F(w * p[j])) % mod;
}
}
printf("%lld\n", (s[get(n)] + 1) % mod);
return 0;
}
可以看出方法二实际效率确实劣于方法一。
例题
求质数个数,联想到 min_25 筛的第一步。相当于求 这个函数在质数处取值的前缀和。
容易发现两题本质相同,以第二道为例。
求合法的质数个数,容易联想到 min_25 筛的第一步。可以设 表示前 个数中,最小质因子 ,且 的数的个数。转移的时候,设 ,那么枚举 ,把 的状态贡献到 的状态即可。
考虑函数质数处的取值。容易发现,。
第一步中,应当直接把 当成 ,然后拆成 计算,最后整合起来,并特判含 的前缀和。注意,我们应当保证拆成的若干个函数是完全积性函数,因此 这样的拆分是不可以的。
第二步没有特别之处,套用模板即可。
观察发现,当 时,。当 时 。接下来考虑 且 的情况。
根据 的定义知,此时 可以表示成 。而 取 还是 取决于 的奇偶性。我们发现, 当且仅当 中包含奇数个质因子,因此上式等价于 。由简单组合知识可知这个式子等于 。于是,当 即 为质数时,,否则 。
我们惊奇地发现 。所以要求 ,就是求 。
前一项是积性函数前缀和,可以 min_25 筛。后一项显然套用 min_25 筛的第一步即可。
容易知道就是求 ,其中 表示 的因数个数。展开变成 。
对于第一项, 是积性函数,且 ,考虑 min_25 筛。第一步要求函数是完全积性函数,所以提一个 出来,,最后再乘回去。
对于第二项,枚举因数:。数论分块即可。
就是说要知道所有 的值。刚好 min_25 筛的第二种写法可以求出这些东西。
有没有大佬教教我正解的啊(悲)
小结
好像重要的点在简介里都提到了。
参考资料
- https://www.luogu.com.cn/blog/wucstdio/solution-p5325
- https://oi-wiki.org/math/number-theory/min-25/
- 集训队论文:朱震霆 《一些特殊的数论函数求和问题 》
- 集训队论文:任之洲《积性函数求和的几种方法》
相关推荐
评论
共 39 条评论,欢迎与作者交流。
正在加载评论...