[笔记]杜教筛 & Powerful Number 筛

杜教筛

杜教筛的作用

杜教筛可以快速求出积性函数前缀和。如 \(\varphi\)\(\mu\) 等。

什么是杜教筛

定义 \(f(x)\) 为一个积性函数,求 \(F(x) = \sum \limits_{i = 1}^{n} f(x)\)

考虑构造函数 \(h, g\),使得 \(h = f * g\),即 \(h(n) = \sum \limits_{d | n} f(d) g(\dfrac{n}{d}) = \sum \limits_{xy = n} f(x)g(y)\)

先求一下 \(h\) 的前缀和:

\[\begin{array}{c} \sum \limits_{i \le n} h(i) = \sum \limits_{i \le n} \sum \limits_{xy = i} g(x)f(y) \\ = \sum \limits_{x \le n} g(x) \sum \limits_{xy \le n} f(y) \\ = \sum \limits_{x \le n} g(x) \sum \limits_{y = 1} ^ {\lfloor \frac{n}{x} \rfloor} f(y) \\ = \sum \limits_{x \le n} g(x) F(\left \lfloor \dfrac{n}{x} \right \rfloor) \end{array} \]

将原式裂项,得到 \(\sum \limits_{x \le n} g(x) F(\left \lfloor \dfrac{n}{x} \right \rfloor) = g(1)F(n) + \sum \limits_{2 \le x \le n} g(x) F(\left \lfloor \dfrac{n}{x} \right \rfloor)\)

\(h\) 的前缀和为 \(H\),则有 \(H(n) = g(1)F(n) + \sum \limits_{2 \le x \le n} g(x) F(\left \lfloor \dfrac{n}{x} \right \rfloor)\)

移项得:\(g(1)F(n) = H(n) - \sum \limits_{2 \le x \le n} g(x) F(\left \lfloor \dfrac{n}{x} \right \rfloor)\)

因此可以得到杜教筛的一些性质:

对于减号后面的部分,可以直接数论分块求,前提是可以快速求出 \(g\) 的前缀和 \(G\)。对于减号前面的部分,需要快速求出 \(h\) 的前缀和 \(H\)。那么 \(F\) 数组在等号前后都用到了怎么办啊?没事,递归 + 记忆化搜索完事。

因此可以得出杜教筛的一些性质:

  • 待积 \(f\) 需要找到一组函数 \(g, h\),满足 \(h = f * g\)

  • \(h, g\) 的前缀和需要告诉求出。

根据上面的式子,也可以写出杜教筛的常用代码:

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;
}

其时间复杂度为 \(O(n ^ {\frac{3}{4}})\)

杜教筛复杂度证明

设算法复杂度 \(T(n)\),考虑到算法的复杂度实际为整除分块后递归的复杂度,以及递归后合并的复杂度。故有:

\[\begin{array}{c} T(n) = O(\sqrt{n}) + O\left(\sum \limits_{i = 2}^{\sqrt{n}} T\left(\left \lfloor \dfrac{n}{i} \right \rfloor \right)\right) \\\\ T\left(\left \lfloor \dfrac{n}{i} \right \rfloor \right) = O\left(\sqrt{\left \lfloor \dfrac{n}{i} \right \rfloor}\right) + O\left( \sum \limits_{j = 2}^{\sqrt{\frac{n}{i}}} T\left(\left \lfloor \dfrac{n}{ij} \right \rfloor \right) \right) \end{array} \]

其中 \(O\left( \sum \limits_{j = 2}^{\sqrt{\frac{n}{i}}} T\left(\left \lfloor \dfrac{n}{ij} \right \rfloor \right) \right)\) 可以看作 \(O\) 意义下的高阶无穷小量。可以略去。

故有:

\[\begin{array}{c} T\left(\left \lfloor \dfrac{n}{i} \right \rfloor\right) = O\left(\sqrt{\left \lfloor \dfrac{n}{i} \right \rfloor}\right) \\\\ T(n) = O(\sqrt{n}) + O\left( \sum \limits_{i = 2}^{\sqrt{n}} \sqrt{\left \lfloor \dfrac{n}{i} \right \rfloor} \right) = O\left( \sum \limits_{i = 2}^{\sqrt{n}} \sqrt{\left \lfloor \dfrac{n}{i} \right \rfloor} \right)\\\\ \end{array} \]

利用积分换求和的技巧,可以得到:

\[\begin{array}{c} T(n) = O\left( \sum \limits_{i = 2}^{\sqrt{n}} \sqrt{\left \lfloor \dfrac{n}{i} \right \rfloor} \right) = O\left( \int_{0}^{\sqrt{n}} \sqrt{\dfrac{n}{x}} \mathrm{dx} \right)\\\\ \end{array} \]

做一下这个积分,可以发现等于 \(O(n ^ {\frac{3}{4}})\)

例题 1

\(\text{Luogu P4213}\)

\(\mu, \varphi\) 函数的前缀和。

套用刚才的式子,我们知道需要找到一个 \(g, h\),使得 \(h = g * \mu\)。可以知道,\(g = I, h = \epsilon\) 时,有 \(\mu * I = \epsilon\)。而 \(I\) 的前缀和最好求不过了,就是 \(n\)\(\epsilon\) 的前缀和就是 \(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;
}

对于 \(\varphi\),熟悉莫反应该知道一个式子:\(\varphi * I = id\)。所以选取 \(h = id, g = I\) 即可。其中 \(I\) 的前缀和前面已经说过,而 \(id\) 的前缀和就是等差数列求和公式,为 \(\dfrac{n (n + 1)}{2}\)

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;
}

欸,怎么只有 \(30\) 分?说明算法可以继续优化。

算法优化

假设假设我们已经用线性筛筛出了前 \(k\) 项函数前缀和,即预处理了 \(F(1) \sim F(k)\)。那么对于 \(\left \lfloor \dfrac{n}{i} \right \rfloor \le k\) 的部分都不需要递归计算了。从而新算法复杂度(这里指递归计算的部分)\(T'(n) = O\left( \sum \limits_{i = 2}^{\frac{n}{k}} \sqrt{\left \lfloor \dfrac{n}{i} \right \rfloor} \right) = O\left( \int_{i = 0}^{\frac{n}{k}} \sqrt{\left \lfloor \dfrac{n}{i} \right \rfloor} \right) = O\left( \dfrac{n}{\sqrt{k}}\right)\)

所以算法时间复杂度就是 \(T(n) = O(k) + T'(n) = O(k) + O\left( \dfrac{n}{\sqrt{k}}\right)\)

根据均值不等式可得,当 \(k = \dfrac{n}{\sqrt{k}}\),即 \(k = n ^ {\frac{2}{3}}\) 时,时间复杂度最优为 \(O(n ^ {\frac{2}{3}})\)

因此对于模板题,只需要筛出约前 \(6000000\)\(\varphi\)\(\mu\) 的前缀和就可以了。

#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

简称 \(\text{PN}\)\(\text{PN}\) 是指正整数 \(n\),满足 \(n\) 的分解 \(\prod p_i ^ {c_i}\) 中,\(\forall i \in [1, k], c_i > 1\)

暂且假设 \(\text{Powerful Number}\) 组成的集合为 \(\mathbb{NP}\)

引理 \(1\)\(\forall n \in \mathbb{NP}\)\(n\) 可以表示成 \(a ^ 2 b ^ 3\) 的形式。

证明很简单,把 \(n\) 分解成 \(\prod p_i ^ {c_i}\) 形式后,如果 \(c_i\) 是偶数就归到 \(a ^ 2\) 类里,否则就减三再扔到 \(a ^ 2\) 类里面去。其本质是任意 \(\ge 2\) 的正整数都可以表示成 \(2a + 3b\) 的形式。

引理 \(2\)\(n\) 以内的 \(\text{PN}\) 数量为 \(O(\sqrt{n})\) 级别。

证明:转化成积分形式就是 \(\int _ 0 ^ {\sqrt{n}} \left ( \dfrac{n}{x ^ 2} \right ) ^ {\frac{1}{3}} \mathrm{dx}\)

积出来以后是 \(O(\sqrt{n})\) 级别的。

Powerful Number 筛

下面的函数均指积性函数。

对于 \(f(x)\) 求前缀和,记为 \(F(x)\)\(F(n) = \sum \limits_{i = 1}^{n} f(i)\)

考虑构造函数 \(g, h\),使得 \(f = g * h\),且使 \(g\)\(x \in \mathbb{P}\) 的时候取值与 \(f(x)\) 相同。

考虑当 \(n \in \mathbb{P}\) 时,\(f(n) = \sum \limits_{d | n} g(d) h (\dfrac{n}{d}) = g(1)h(n) + g(n)h(1)\)。由于 \(g(n) = f(n), g(1) \ne 0\),所以 \(h(n) = 0\)。由于 \(h\) 为积性函数,可以得到当 \(n \notin \mathbb{NP}\) 时,\(h(n)\) 均等于 \(0\)

所以继续化简式子:

\[\begin{array}{c} F(n) = \sum \limits_{i \le n}^{} f(i) \\\\ = \sum \limits_{i \le n} g * h \\\\ = \sum \limits_{i \le n}^{} \sum \limits_{xy = i} g(x)h(y) \\\\ = \sum \limits_{x \le n} h(x) \sum \limits_{y \le \frac{n}{x}} g(y) \\\\ = \sum \limits_{x \le n} h(x) G(\dfrac{n}{x})\\\\ = \sum \limits_{x \le n \& x \in \mathbb{NP}} h(x) G(\dfrac{n}{x}) \end{array} \]

也就是说,我们只要能够快速算出 \(n\) 以内的所有 \(\text{Powerful Number}\),并且能够快速计算 \(G\) 的前缀和即可。

其中 \(G\) 的前缀和可以直接杜教筛做到 \(O(n ^ {\frac{2}{3}})\) 的复杂度,而 \(h\) 的点值可以直接爆搜所有 \(\text{Powerful Number}\)

算法复杂度为 \(O\left(\dfrac{n ^ {\frac{3}{4}}}{\log n} \right)\)

例题

Min25 筛模板

\(f(p ^ k) = p ^ k (p ^ k - 1)\)

考虑设计 \(g(p) = p \times \varphi(p) = p \times (p - 1) = f(p)\)

所以 \(g(x) = x \times \varphi(x)\)

再设计一个 \(h(x)\),使得 \(f = g * h\)。不想写了,反正就是 \(h(p ^ k) = (k - 1)(p - 1)p ^ k\)

#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;
}
posted @ 2024-11-28 21:22  Link-Cut-Y  阅读(1)  评论(0编辑  收藏  举报