[笔记]杜教筛 & Powerful Number 筛
杜教筛
杜教筛的作用
杜教筛可以快速求出积性函数前缀和。如 , 等。
什么是杜教筛
定义 为一个积性函数,求 。
考虑构造函数 ,使得 ,即 。
先求一下 的前缀和:
将原式裂项,得到 。
设 的前缀和为 ,则有 。
移项得:。
因此可以得到杜教筛的一些性质:
对于减号后面的部分,可以直接数论分块求,前提是可以快速求出 的前缀和 。对于减号前面的部分,需要快速求出 的前缀和 。那么 数组在等号前后都用到了怎么办啊?没事,递归 + 记忆化搜索完事。
因此可以得出杜教筛的一些性质:
-
待积 需要找到一组函数 ,满足 。
-
的前缀和需要告诉求出。
根据上面的式子,也可以写出杜教筛的常用代码:
void calc(int n) {
if (n == 1) return f(1);
if (sum[n]) return sum[n];
int ans = H(n);
for (int l = 2, r; l <= n; r = l + 1) {
r = n / (n / l);
ans -= (r - l + 1) * calc(n / l);
} return sum[n] = ans;
}
其时间复杂度为 。
杜教筛复杂度证明
设算法复杂度 ,考虑到算法的复杂度实际为整除分块后递归的复杂度,以及递归后合并的复杂度。故有:
其中 可以看作 意义下的高阶无穷小量。可以略去。
故有:
利用积分换求和的技巧,可以得到:
做一下这个积分,可以发现等于 。
例题 1
求 函数的前缀和。
套用刚才的式子,我们知道需要找到一个 ,使得 。可以知道, 时,有 。而 的前缀和最好求不过了,就是 。 的前缀和就是 。所以可以写出这样的代码:
unordered_map<int, int> mus;
int get_mu(int n) {
if (n == 1) return 1ll;
if (mus[n]) return mus[n];
int ans = 1ll;
for (int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
ans -= (r - l + 1) * get_mu(n / l);
} return mus[n] = ans;
}
对于 ,熟悉莫反应该知道一个式子:。所以选取 即可。其中 的前缀和前面已经说过,而 的前缀和就是等差数列求和公式,为 。
unordered_map<int, int> phs;
int get_phi(int n) {
if (n == 1) return 1ll;
if (phs[n]) return phs[n];
int ans = n * (n + 1) >> 1;
for (int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
ans -= (r - l + 1) * get_phi(n / l);
} return phs[n] = ans;
}
总代码如下:
#include <unordered_map>
#include <iostream>
#include <cstring>
#include <cstdio>
#define int long long
using namespace std;
int n;
unordered_map<int, int> mus;
int get_mu(int n) {
if (n == 1) return 1ll;
if (mus[n]) return mus[n];
int ans = 1ll;
for (int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
ans -= (r - l + 1) * get_mu(n / l);
} return mus[n] = ans;
}
unordered_map<int, int> phs;
int get_phi(int n) {
if (n == 1) return 1ll;
if (phs[n]) return phs[n];
int ans = n * (n + 1) >> 1;
for (int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
ans -= (r - l + 1) * get_phi(n / l);
} return phs[n] = ans;
}
signed main() {
int T; scanf("%d", &T);
while (T -- ) {
mus.clear();
phs.clear();
scanf("%lld", &n);
printf("%lld %lld\n", get_phi(n), get_mu(n));
} return 0;
}
欸,怎么只有 分?说明算法可以继续优化。
算法优化
假设假设我们已经用线性筛筛出了前 项函数前缀和,即预处理了 。那么对于 的部分都不需要递归计算了。从而新算法复杂度(这里指递归计算的部分)
所以算法时间复杂度就是
根据均值不等式可得,当 ,即 时,时间复杂度最优为 。
因此对于模板题,只需要筛出约前 个 和 的前缀和就可以了。
#include <unordered_map>
#include <iostream>
#include <cstring>
#include <cstdio>
#define int long long
using namespace std;
const int N = 6000010;
int n, lim;
int p[N], mu[N], phi[N];
int smu[N], sphi[N], cnt;
bool is_prime[N];
unordered_map<int, int> mus;
void init(int n) {
mu[1] = phi[1] = 1;
for (int i = 2; i <= n; i ++ ) {
if (!is_prime[i]) p[ ++ cnt] = i, phi[i] = i - 1, mu[i] = -1;
for (int j = 1; j <= cnt and p[j] * i <= n; j ++ ) {
is_prime[i * p[j]] = true;
if (i % p[j] == 0) {
phi[i * p[j]] = phi[i] * p[j];
break;
}
mu[i * p[j]] = - mu[i];
phi[i * p[j]] = phi[i] * phi[p[j]];
}
}
for (int i = 1; i <= n; i ++ )
sphi[i] = sphi[i - 1] + phi[i],
smu[i] = smu[i - 1] + mu[i];
}
int get_mu(int n) {
if (n <= lim) return smu[n];
if (mus[n]) return mus[n];
int ans = 1ll;
for (int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
ans -= (r - l + 1) * get_mu(n / l);
} return mus[n] = ans;
}
unordered_map<int, int> phs;
int get_phi(int n) {
if (n <= lim) return sphi[n];
if (phs[n]) return phs[n];
int ans = n * (n + 1) >> 1;
for (int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
ans -= (r - l + 1) * get_phi(n / l);
} return phs[n] = ans;
}
signed main() {
int T; scanf("%d", &T);
lim = 6000000;
init(lim);
while (T -- ) {
mus.clear();
phs.clear();
scanf("%lld", &n);
printf("%lld %lld\n", get_phi(n), get_mu(n));
} return 0;
}
Powerful Number 筛
Powerful Number
简称 。 是指正整数 ,满足 的分解 中,。
暂且假设 组成的集合为 。
引理 :, 可以表示成 的形式。
证明很简单,把 分解成 形式后,如果 是偶数就归到 类里,否则就减三再扔到 类里面去。其本质是任意 的正整数都可以表示成 的形式。
引理 : 以内的 数量为 级别。
证明:转化成积分形式就是 。
积出来以后是 级别的。
Powerful Number 筛
下面的函数均指积性函数。
对于 求前缀和,记为 。。
考虑构造函数 ,使得 ,且使 在 的时候取值与 相同。
考虑当 时,。由于 ,所以 。由于 为积性函数,可以得到当 时, 均等于 。
所以继续化简式子:
也就是说,我们只要能够快速算出 以内的所有 ,并且能够快速计算 的前缀和即可。
其中 的前缀和可以直接杜教筛做到 的复杂度,而 的点值可以直接爆搜所有 。
算法复杂度为
例题
考虑设计 。
所以 。
再设计一个 ,使得 。不想写了,反正就是 。
#include <unordered_map>
#include <iostream>
#include <cstring>
#include <cstdio>
#define int long long
using namespace std;
const int N = 3000010;
const int mod = 1e9 + 7;
unordered_map<int, int> Gsum;
int phi[N], p[N], s[N];
int lim, ans, n, cnt, inv2, inv6;
bool is_prime[N];
int Mod(int a, int b) {
return ((a % mod) * (b % mod)) % mod;
}
int power(int a, int b = mod - 2) {
int ans = 1;
for (; b; b >>= 1, a = a * a % mod)
if (b & 1) ans = ans * a % mod;
return ans;
}
void init(int n) {
phi[1] = 1;
for (int i = 2; i <= n; i ++ ) {
if (!is_prime[i]) p[ ++ cnt] = i, phi[i] = i - 1;
for (int j = 1; j <= cnt and i * p[j] <= n; j ++ ) {
is_prime[i * p[j]] = true;
if (i % p[j] == 0) { phi[i * p[j]] = phi[i] * p[j]; break; }
phi[i * p[j]] = phi[i] * phi[p[j]];
}
}
for (int i = 1; i <= n; i ++ )
s[i] = (s[i - 1] + i * phi[i] % mod) % mod;
}
int get_G(int n) {
if (n <= lim) return s[n];
if (Gsum[n]) return Gsum[n];
int ans = Mod(Mod(Mod(n, n + 1), (n * 2 % mod + 1)), inv6);
for (int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
ans -= Mod(Mod(Mod(l % mod + r % mod, r - l + 1), inv2), get_G(n / l));
if (ans < 0) ans += mod;
}
return Gsum[n] = ans;
}
void dfs(int now, int num, int val) {
if (now > cnt or num * p[now] > n) {
if (n / num <= lim) ans = (ans + Mod(val, s[n / num])) % mod;
else ans = (ans + Mod(val, Gsum[n / num])) % mod;
return;
}
dfs(now + 1, num, val);
int u = Mod(Mod((p[now] - 1), p[now]), p[now]), tmp = p[now] * p[now];
for (int i = 2; num * tmp <= n; i ++ , tmp = tmp * p[now]) {
dfs(now + 1, num * tmp, Mod(Mod(val, i - 1), u));
u = Mod(u, p[now]);
}
}
signed main() {
inv2 = power(2);
inv6 = power(6);
scanf("%lld", &n);
lim = 3000000;
init(lim); get_G(n);
dfs(1, 1, 1);
printf("%lld\n", ans);
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库