Min_25 筛
前置知识:积性函数,数论分块。
1 概述
前面我们学习过杜教筛,它可以在亚线性的复杂度内求出一些数论函数的前缀和。而 Min_25 筛同样可以在亚线性复杂度内求出一些积性函数的前缀和。
考虑我们朴素的筛法求积性函数值无法低于线性复杂度的原因就是其必须枚举区间内的每一个数,而 Min_25 筛则是将区间内的所有数分为质数和合数两类进行处理,然后相加得出答案。于是 Min_25 筛的使用对于求解的积性函数
2 算法思想
2.1 分类
上面说过,Min_25 筛的重点在于将所有数分为质数和合数两类,也就是:
然后进一步的,我们枚举后面合数的最小质因子以及质因子的次数,可以得到:
其中
这个分类思想是 Min_25 筛中一个重要的思想,在最后求解答案时会再次用到。
2.2 质数求解
由于 Min_25 筛求解的
考虑利用埃氏筛的思路,同时考虑一个 dp,设
考虑怎样转移,我们考虑从
既然最小质因子是
这里我们能直接提出
不过现在直接暴力求解的复杂度仍然没有降下去。考虑中间的一项
不过式子中还有一项是
此处有一个命题:
。 考虑反证,设存在一个
,满足 ,则 不可被表示。于是有 ,即 。代回去后可得 。 另一方面来讲,由于
,所以 ,即 。由于 ,所以 ,于是 ,所以 。 综合可得
,显然不成立,于是原命题成立。
注意这里的
2.3 合数求解
2.3.1 方法一
和上面类似的,设
考虑按照最开始分类时的思路,将这个贡献拆成质数的贡献和合数的贡献。质数贡献容易计算,就是
注意枚举的
求解的话直接暴力递归求解即可。
2.3.2 方法二
我们可以直接套用求解质数的状态,设
枚举时依然需要保证
但是再细想一下就会发现不对,如果
此时需要保证
不管采用什么方法,可以证明,Min_25 筛的总复杂度是
2.4 代码
模板题:【模板】Min_25 筛。题目中已经告诉我们
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
const int Mod = 1e9 + 7;
const int Inv2 = 500000004;
const int Inv6 = 166666668;
int n, m;
int prim[Maxn], cnt, vis[Maxn];
void init(int N) {//预处理质数
for(int i = 2; i <= N; i++) {
if(!vis[i]) {
prim[++cnt] = i;
}
for(int j = 1; prim[j] * i <= N; j++) {
vis[i * prim[j]] = 1;
if(i % prim[j] == 0) break;
}
}
}
int ind1[Maxn], ind2[Maxn];
//分别存储 x 和 n/x 在 dp 数组的编号,这里 x <= sqrt(n)
int g1[Maxn], g2[Maxn], val[Maxn], tot;
//一次项和二次项之和 所有 n/x 的值
int getid(int x) {return x <= m ? ind1[x] : ind2[n / x];}//获取当前值的编号
int F(int x) {x %= Mod; return (x * x % Mod - x + Mod) % Mod;}
int S(int n, int j) {//递归求解 S(n,j)
if(prim[j] > n) return 0;//递归边界,直接返回
int id1 = getid(n), id2 = getid(prim[j]);
int res = (g2[id1] - g1[id1]) - (g2[id2] - g1[id2]);
res = (res % Mod + Mod) % Mod;//算出 G(n)-G(p[j])
for(int k = j + 1; k <= cnt && prim[k] <= n / prim[k]; k++) {
int num = prim[k];
for(int e = 1; num <= n; e++, num = num * prim[k]) {
res = (res + F(num) * (S(n / num, k) + (e > 1)) % Mod) % Mod;
//按照递归式直接递归即可
}
}
return res;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
m = sqrt(n);
init(m);
int l = 1, r;
while(l <= n) {//数论分块求出所有值
r = n / (n / l);
val[++tot] = n / l;
if(val[tot] <= m) ind1[val[tot]] = tot;
else ind2[n / val[tot]] = tot;
int num = val[tot] % Mod;//注意要先取模
g1[tot] = num * (num + 1) % Mod * Inv2 % Mod - 1;
g2[tot] = num * (num + 1) % Mod * (2 * num + 1) % Mod * Inv6 % Mod - 1;
//先求出所有 g(x,0),显然就是前缀和
//减一是为了减掉 1 的贡献
l = r + 1;
}
for(int j = 1; j <= cnt; j++) {//先枚举第二维
for(int i = 1; i <= tot && prim[j] <= val[i] / prim[j]; i++) {//枚举第一维
int id1 = getid(val[i] / prim[j]), id2 = getid(prim[j - 1]);
g1[i] -= prim[j] * (g1[id1] - g1[id2]) % Mod;
g2[i] -= prim[j] * prim[j] % Mod * (g2[id1] - g2[id2]) % Mod;
g1[i] = (g1[i] % Mod + Mod) % Mod;
g2[i] = (g2[i] % Mod + Mod) % Mod;//按照式子求解
}
}
cout << (S(n, 0) + 1) % Mod << '\n';//求出答案之后记得加一
return 0;
}
采用方法二代码如下:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
const int Mod = 1e9 + 7;
const int Inv2 = 500000004;
const int Inv6 = 166666668;
int n, m;
int prim[Maxn], cnt, vis[Maxn];
void init(int N) {
for(int i = 2; i <= N; i++) {
if(!vis[i]) prim[++cnt] = i;
for(int j = 1; prim[j] * i <= N; j++) {
vis[i * prim[j]] = 1;
if(i % prim[j] == 0) break;
}
}
}
int ind1[Maxn], ind2[Maxn];
int val[Maxn], tot, g1[Maxn], g2[Maxn];
int S[Maxn], G[Maxn];
int getid(int x) {return x <= m ? ind1[x] : ind2[n / x];}
int F(int x) {x %= Mod; return (x * x % Mod - x + Mod) % Mod;}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
m = sqrt(n);
init(m);
int l = 1, r;
while(l <= n) {
r = n / (n / l);
val[++tot] = n / l;
if(val[tot] <= m) ind1[val[tot]] = tot;
else ind2[n / val[tot]] = tot;
int num = val[tot] % Mod;
g1[tot] = num * (num + 1) % Mod * Inv2 % Mod - 1;
g2[tot] = num * (num + 1) % Mod * (2 * num + 1) % Mod * Inv6 % Mod - 1;
l = r + 1;
}
for(int j = 1; j <= cnt; j++) {
for(int i = 1; i <= tot && prim[j] * prim[j] <= val[i]; i++) {
int id1 = getid(val[i] / prim[j]), id2 = getid(prim[j - 1]);
g1[i] -= prim[j] * (g1[id1] - g1[id2]) % Mod;
g2[i] -= prim[j] * prim[j] % Mod * (g2[id1] - g2[id2]) % Mod;
g1[i] = (g1[i] % Mod + Mod) % Mod, g2[i] = (g2[i] % Mod + Mod) % Mod;
}
}
//到这里都与方法一一致
for(int i = 1; i <= tot; i++) {
S[i] = G[i] = (g2[i] - g1[i] + Mod) % Mod;
}
for(int j = cnt; j >= 1; j--) {
for(int i = 1; i <= tot && prim[j] * prim[j] <= val[i]; i++) {
int num = prim[j];
for(int e = 1; num <= val[i] / prim[j]; e++, num = num * prim[j]) {
int id1 = getid(val[i] / num), id2 = getid(prim[j]);
(S[i] += F(num) * (S[id1] - G[id2] + Mod) % Mod + F(num * prim[j])) %= Mod;//按照递推式直接求 S
}
}
}
cout << (S[1] + 1) % Mod << '\n';
return 0;
}
3 例题
例 1 [LOJ6235] 区间素数个数
很简单,我们只需要求出
例 2 [LOJ6053] 简单的函数
实际上还有 [LOJ6783 ~ 6785] 简单的函数 加强版,
这个函数很有意思,首先它满足在质数的幂处可以简单求解,不过在
不过我们完全可以将
后面三道加强版的题就需要用方法二求出所有
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律