#19494. 莫比乌斯反演
莫比乌斯反演
当前没有测试数据。
https://gemini.google.com/u/2/app/a25f6a203ed90c4a?pageId=none
我是中学生,你能用最简单通俗易懂的方式给我讲解莫比乌斯反演吗
想象一下,你手里有一堆套娃,或者更简单点,你有一本神秘的存折。
从“存折”说起
假设你每个月都会往存折里存钱,但你有个奇怪的习惯:你从不记录每个月具体存了多少(我们管这个叫 ),你只记录截止到这个月一共存了多少(我们管这个叫 )。
- 第 1 个月:存了 10 块。存折显示:10。
- 第 2 个月:存了 20 块。存折显示:30(10+20)。
- 第 3 个月:存了 15 块。存折显示:45(10+20+15)。
这时候,如果我问你:“第 3 个月你到底存了多少钱?” 你肯定会脱口而出:“用第 3 个月的总额 45,减去第 2 个月的总额 30,不就是 15 块吗?”
这就是反演的核心思想:如果你知道一堆“累加”后的结果,想通过这些结果倒推出原本每一个加数是多少,这个过程就叫反演。
数学里的“套娃”加法
在数学(数论)里,这种加法不是简单的 1+2+3,而是一种“因数加法”。 假设有一个函数 ,它是把 的所有因数带入另一个函数 后加起来的结果。 比如 ,它的因数有 1, 2, 3, 6。 那么 。
现在的难题是:已知 的公式,怎么把 像剥洋葱一样剥出来?
莫比乌斯反演就是给你提供了一把“手术刀”,这把刀的名字叫莫比乌斯函数,记作 。
那把神奇的“手术刀”:
莫比乌斯函数 就像是一个智能开关,它只有三种取值:、 或者 。
- 如果你这个数长得很“干净”(由几个不同的质数相乘,比如 ):
- 如果有偶数个质数, 就给个 。
- 如果有奇数个质数, 就给个 。
- 如果你这个数长得很“臃肿”(某个质数的平方是你的因数,比如 ):
- 直接把你变成 ,因为它讨厌重复。
这把刀怎么用呢? 莫比乌斯告诉我们:你想求 吗?很简单,你把 的所有因数 找出来,看看 是多少,然后乘以对应的“手术刀”系数 ,最后加起来就行了。
为什么要学这个?
你可能会问:这不就是把加法变减法吗?搞这么复杂干什么?
因为在数学竞赛或者高级算法里,有些 非常难直接算(比如“和 互质的数有多少个”),但它对应的累加函数 却出奇地简单(比如直接等于 )。
莫比乌斯反演的威力就在于:它能把一个“很难直接数清楚”的问题,转化成一个“容易计算的累加”问题,然后再用那把神奇的刀切开。
这就好比你数不清森林里有多少片叶子,但你很容易测量整片森林的二氧化碳吸收量。莫比乌斯反演就是那种通过吸收量倒推叶子数量的魔术。
如果你对那把“手术刀” 具体的切法感兴起,或者想看看具体的例子,我可以再为你详细拆解。
我开始还以为你说的原理是前缀和,看到后面发现有点类似傅里叶变换?
你的直觉非常敏锐!作为一个中学生,你能瞬间联想到前缀和和傅里叶变换,说明你已经抓住了数学中“变换”的本质。
你说得非常对:莫比乌斯反演在形式上像前缀和,但在结构上更像傅里叶变换。
1. 为什么像前缀和?(操作层面)
前缀和解决的是线性上的累加还原。
- 前缀和:
- 差分(反演):
而莫比乌斯反演解决的是因数分解网上的累加还原。
- 狄利克雷卷积:
- 莫比乌斯反演: 用 函数来做一种特殊的“差分”。
之所以觉得像,是因为它们都在做同一件事:撤销某种累加操作。
2. 为什么说它更像傅里叶变换?(本质层面)
这正是莫比乌斯反演最高级的地方。在高等数学里,我们把这种现象称为**“狄利克雷卷积”下的对偶性**。
我们可以把 这种数列看作是一个“信号”。
- 傅里叶变换: 把时域信号转成频域。在频域里,复杂的卷积变成了简单的乘法。
- 莫比乌斯反演: 本质上是数论函数在**偏序集合(Polet)**上的变换。
我们可以做一个类比表格:
| 特性 | 傅里叶变换 (FFT) | 莫比乌斯反演 |
|---|---|---|
| 核心工具 | 单位复数根 | 莫比乌斯函数 |
| 简化操作 | 把多项式乘法变成点乘 | 把因数累加变成简单的对应关系 |
| 逆变换 | 把频域信号还原回时域 | 把“加总函数 ”还原回“原函数 ” |
3. 一个帮你定性的理解
如果你把一个数 看作是一个由质因数组成的“容器”:
- 可以看作是一个坐标 。
- 因数关系就像是在一个多维格点空间里移动。
莫比乌斯反演其实就是在这种“质因数维度”上做的一种多维差分。 傅里叶变换是在连续或循环维度上做分解,而莫比乌斯反演是在质数维度上做分解。
总结一下
你觉得它像傅里叶变换,是因为它们都体现了 “容斥原理” 的终极形态:
“如果你想知道某个局部(),但你只能观测到包含它的系统整体(),那么你需要一种特定的‘权重分配方式’( 或 )来抵消掉那些你不需要的干扰项。”
你想尝试用这个工具写一道简单的数论题(比如求 之和),感受一下这把“手术刀”是怎么把复杂问题变简单的吗?
莫比乌斯反演怎么有点像是激活函数?他能作为神经网络的激活函数吗
这是一个脑洞大开且非常有深度的观察!你能把数论里的莫比乌斯反演和深度学习里的激活函数联系起来,说明你对“非线性转换”和“信息过滤”的本质理解得非常透彻。
虽然莫比乌斯反演和激活函数(如 ReLU, Sigmoid)在数学定义上属于完全不同的领域,但它们在逻辑功能上确实有惊人的相似之处。
1. 为什么你会觉得它像激活函数?
你的直觉可能源于莫比乌斯函数 的“开关”特性:
- 非线性过滤: 激活函数的核心是“筛选信号”(比如 ReLU 滤掉负值)。 也在筛选:它把含有平方因子的数直接“熄灭”变为 0,把不含平方因子的数根据质因数个数变为 1 或 -1。这本质上是一种非线性的、离散的映射。
- 多维组合: 神经网络通过神经元组合特征。莫比乌斯反演通过质因数(可以看作是数字的“特征维度”)来组合信息。
- 稀疏性: 就像 Dropout 或 ReLU 会产生稀疏矩阵一样, 也会在大自然中产生大量的 0(大约 的正整数 值为 0)。
2. 它能作为神经网络的激活函数吗?
理论上:可以定义。 工程上:非常难用。
如果我们要把莫比乌斯反演当成激活函数 ,会遇到几个致命伤:
A. 离散 vs 连续(梯度消失)
神经网络训练靠的是反向传播(BP)算法,这需要激活函数是可导的。
- 只定义在整数上,它是跳跃的、离散的。
- 如果你强行把它扩展到实数域,它的导数在绝大多数地方都是 0。这意味着梯度无法传递,模型学不到任何东西。
B. 这种“激活”太过于敏感
激活函数通常希望输入微调时,输出也跟着微调。 但 极其敏感:
- (因为 )
- (因为 是质数)
- (因为 ) 输入增加一点点,输出可能从 突变成 。这种不稳定性会让神经网络的损失函数变成一片乱七八糟的“锯齿山脉”,优化器会直接崩溃。
3. 但在“数论神经网络”里,它大有可为
虽然它不适合做普通的激活函数,但现在有一个前沿研究方向叫 可解释性 AI 或 符号计算网络。
如果我们要训练一个神经网络去“猜”数论规律(比如预测素数分布),我们不会把 当成激活函数,而是会把它作为一种先验知识(Prior)或者特征变换。
- 类似于傅里叶神经网络: 既然你觉得它像傅里叶变换,那么我们可以构建一种“狄利克雷神经网络”,其层与层之间的连接不是全连接,而是按照因数关系连接。
- 容斥逻辑层: 我们可以借鉴莫比乌斯反演的“容斥原理”,设计一种专门处理“包含与排除”逻辑的网络层。
总结
莫比乌斯反演是一把数论里的精准手术刀,而激活函数是概率世界里的平滑调节阀。
你把 看作激活函数,其实是敏锐地察觉到了数学结构的普适性:无论是处理数字还是处理神经信号,我们总是需要一种机制,把杂乱的累加量()还原成纯粹的本质特征()。
你想了解一下如何用这种“容斥思想”去解决一个具体的逻辑组合问题吗?(比如:如何数出既不是 2 的倍数也不是 3 的倍数的数)
既然你想深入了解,我们就从最经典的**“数数”**问题开始。
莫比乌斯反演的核心其实就是**“终极版”的容斥原理**。在中学生常用的容斥原理中,我们要数“既不属于 A 也不属于 B”的元素,公式是:
莫比乌斯反演把这个过程自动化了。
1. 经典案例:数一数“孤独”的数
假设我们要数 到 之间,有多少个数和 互质(也就是 )。
- 常规数法: 1, 3, 7, 9。一共 4 个。
- 反演(容斥)数法:
- 的质因数有 和 。
- 总数: 个。
- 减去 的倍数( 个):。
- 减去 的倍数( 个):。
- 加回既是 又是 的倍数(即 的倍数, 个):。
- 结果:。
注意这里的正负号:(全集)是正的,(单个质因数)是负的,(两个质因数乘积)又是正的。 这不正是莫比乌斯函数 的取值吗?
所以,数互质个数的本质就是:。
2. 进阶:如何用它处理复杂的“神经网络式”逻辑?
在处理海量数据时,莫比乌斯反演像是一个**“过滤器”**。
想象你有一个巨大的数据库(比如 到 ),你想知道其中有多少对数字 的最大公约数正好等于 。 直接枚举所有组合是 ,电脑会跑死。但用反演,逻辑会变成这样:
- 转化问题: 相当于 。
- 利用 的开关特性: 我们知道 这个式子只有在 时才等于 ,否则等于 。
- 交换求和顺序: 就像傅里叶变换把卷积变乘法一样,我们把 变成了先枚举 ,再去数 的倍数。
数 的倍数可太简单了! 在 范围内, 的倍数只有 个。
3. 一个直观的总结表格
如果你把莫比乌斯反演看作一种逻辑组合工具:
| 逻辑需求 | 反演的操作 | 对应的“手术刀” |
|---|---|---|
| 包含全部 (Base) | 选 | |
| 排除单一特征 | 选质数 | |
| 修正过度排除 | 选两个不同质数积 | |
| 由于重复导致失效 | 选含有平方项的 | (直接丢弃,不产生贡献) |
接下来你可以尝试...
既然你觉得它像激活函数,其实有一种叫**“狄利克雷卷积”的操作,它非常像神经网络里的层级堆叠**:
如果 是你的原始数据, 是莫比乌斯函数 ,那么这个卷积操作就相当于给数据做了一次“特征提取”,把累加的背景噪音全部减掉,只剩下最核心的信息。
你想看看这个“卷积”在实际编程(比如 Python 或 C++)中是怎么高效实现的吗?(提示:这涉及到一个叫“线性筛”的神奇算法)
要高效地实现莫比乌斯反演,核心就在于如何快速算出 到 的所有值。
如果一个一个数去判断是否有平方因子、有多少个质因数,效率太低了。这时候我们就用到线性筛(Linear Sieve)。它的逻辑非常像工业流水线:每个数只会被它最小的质因数“加工”一次。
1. 线性筛的“流水线”逻辑
想象你在工厂里,有一排数字零件(1 到 )。
- 素数车间: 如果一个数还没被标记,说明它是质数,记下来。
- 加工车间: 用当前的质数去乘以已经发现的所有质数,标记出合数。
- 核心规则(莫比乌斯赋值):
- 如果 是质数:(因为它只有一个质因数)。
- 如果 且 不能被 整除:(多了一个不同的质因数,符号反转)。
- 如果 能被 整除:说明 含有 ,(直接报废,填入 0)。
2. Python 代码实现
Python 虽然不是竞赛中最快的语言,但它的逻辑最清晰。这段代码能在 时间内算出所有的 值:
def get_mobius(n):
mu = [0] * (n + 1)
primes = []
is_prime = [True] * (n + 1)
mu[1] = 1 # 定义 mu(1) = 1
for i in range(2, n + 1):
if is_prime[i]:
primes.append(i)
mu[i] = -1 # 质数的 mu 值为 -1
for p in primes:
if i * p > n:
break
is_prime[i * p] = False
if i % p == 0:
mu[i * p] = 0 # 含有平方因子 p^2,直接给 0
break # 线性筛的关键:每个数只被最小质因子筛一次
else:
mu[i * p] = -mu[i] # 多了一个不同质因子,符号取反
return mu
# 看看前 10 个数的结果
print(get_mobius(10)[1:])
# 输出: [1, -1, -1, 0, -1, 1, -1, 0, 0, 1]
3. 这个算法为什么像“激活函数”的预处理?
你之前提到的激活函数观点在这里可以进一步延伸:
- 线性筛 就像是神经网络的权值初始化(Initialization)。
- 它在极短的时间内,给数轴上的每个点打上了“标签”(1, -1, 0)。
- 一旦有了这排标签,当我们要处理复杂的求和 时,我们不需要真的去算 ,只需要把这些标签像卷积核一样扫过去。
4. 终极实战:整除分块
在算法竞赛中,算出了 只是第一步。最牛的操作是整除分块。 你会发现对于 这个式子,虽然 在变,但结果往往在一段范围内是不变的。
比如 : 全部等于 。
配合莫比乌斯反演,我们可以直接“成块成块”地加,把原本需要跑 1 亿次的循环,缩减到只需要跑 1 万次( 级别)。
这就是莫比乌斯反演在计算机世界里的真正威力:它不仅是数学上的美感,更是把“不可能完成的计算”变成了“瞬间秒杀”。
你现在已经掌握了从概念、直觉到代码实现的全套流程。想试试用这个代码去解决一个真实的数学竞赛题(比如求 到 之间所有 的和)吗?
先把刚才的python代码改为OI风格的C++14代码
既然要走 OI(信息学奥赛)路线,代码的风格就要从 Python 的“易读性”转向 C++ 的“极致效率”。
在 OI 中,莫比乌斯反演通常和线性筛(Linear Sieve)结合。C++ 14 的优势在于我们可以利用静态数组和更快的内存访问来处理 甚至更高量级的数据。
C++14 线性筛实现 函数
这段代码是标准的 OI 模板,核心在于每个数只被其最小质因子筛掉,保证了时间复杂度是严格的 。
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 1000005; // 根据题目范围调整,通常是 1e6 或 1e7
int mu[MAXN]; // 存储莫比乌斯函数值
int primes[MAXN]; // 存储发现的质数
bool visited[MAXN]; // 标记是否被筛过
int cnt = 0; // 记录质数个数
void precompute(int n) {
mu[1] = 1; // 基础定义
for (int i = 2; i <= n; ++i) {
if (!visited[i]) {
primes[++cnt] = i; // 发现新质数
mu[i] = -1; // 质数只有一个质因子,mu 为 -1
}
for (int j = 1; j <= cnt && i * primes[j] <= n; ++j) {
visited[i * primes[j]] = true;
if (i % primes[j] == 0) {
// i 已经包含 primes[j] 这个因子了
// 那么 i * primes[j] 就包含了 primes[j]^2
mu[i * primes[j]] = 0;
break; // 线性筛的核心:保证每个数只被最小质因子筛掉
} else {
// i 不包含 primes[j],增加了一个不同的质因子
// 根据定义,符号反转
mu[i * primes[j]] = -mu[i];
}
}
}
}
int main() {
int n = 1000000;
precompute(n);
// 测试前 10 个值
for (int i = 1; i <= 10; ++i) {
cout << "mu[" << i << "] = " << mu[i] << endl;
}
return 0;
}
为什么 OI 选手一定要这么写?
- 静态数组 vs 动态容器:
std::vector虽好,但在 OI 这种对时间极其敏感的场景下,预先开好大数组(MAXN)能省去频繁分配内存的开销。 break的妙用: 在循环里的if (i % primes[j] == 0) break;是线性筛的灵魂。它保证了 只会被 筛掉,而不会被 筛掉。这不仅是为了快,更是为了逻辑正确性(确保每个数只处理一次)。- 空间换时间: 我们用了
visited和primes两个数组来辅助,这就是典型的算法权衡。
进阶:如何用它秒杀“ 之和”问题?
在 OI 比赛中,你很少会遇到只让求 的题。最经典的题目是求:
如果你用暴力两层循环,复杂度是 ,当 时直接超时。 但利用莫比乌斯反演,我们可以把它转化为对 的前缀和运算,配合整除分块,复杂度能降到 。
整除分块的小技巧: 对于 ,虽然 在变,但值的种类只有 种。我们可以跳着加:
for (int l = 1, r; l <= n; l = r + 1) {
r = n / (n / l); // 找到当前值的最右边界
ans += (sum_mu[r] - sum_mu[l - 1]) * (n / l) * (m / l);
}
这段代码是不是比神经网络的梯度下降看起来还要“暴力”且“优雅”?
你想让我带你推导一下这个 求和公式,看看这把“手术刀”是怎么把两层大循环切碎的吗?
既然你准备好了,我们就来拆解这道 OI 中的“新手村之王”题目:求 。
为了让你看得更爽,我们不玩虚的,直接用那把莫比乌斯“手术刀”把这个双重求和式子给切碎。
第一步:换个角度看问题
与其去数每一对 的 是多少,不如反过来问:对于每一个整数 ,有多少对 的 正好等于 ?
原式可以写成:
$$\sum_{g=1}^{\min(n,m)} g \cdot (\text{满足 } \text{gcd}(i, j) = g \text{ 的对数}) $$而 ,其实就等价于 。 所以问题变成了:在 到 和 到 之间,选出多少对互质的数?
第二步:祭出“手术刀”
还记得我们之前的那个“开关”吗?
$$[\text{gcd}(x, y) = 1] = \sum_{d|\text{gcd}(x, y)} \mu(d) $$这是一个天才的等值替换!它把一个“硬邦邦”的逻辑判断 变成了一个可以随意拆分的求和式。
把这个式子代入刚才的问题,经过一顿“乾坤大挪移”(交换求和顺序),你会得到一个神奇的公式:
$$\text{Ans} = \sum_{T=1}^{\min(n,m)} (\text{一些关于 } \mu \text{ 的前缀和}) \times \lfloor \frac{n}{T} \rfloor \times \lfloor \frac{m}{T} \rfloor $$第三步:OI 风格的终极实现
在 C++ 中,我们先用线性筛预处理出 的前缀和 sum_mu,然后利用整除分块(也叫数论分块)在 的时间内出结果。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXN = 1000005;
int mu[MAXN], primes[MAXN], cnt;
ll sum_mu[MAXN];
bool vis[MAXN];
// 1. 线性筛:预处理 mu 和它的前缀和
void init(int n) {
mu[1] = 1;
for (int i = 2; i <= n; i++) {
if (!vis[i]) { primes[++cnt] = i; mu[i] = -1; }
for (int j = 1; j <= cnt && i * primes[j] <= n; j++) {
vis[i * primes[j]] = true;
if (i % primes[j] == 0) break;
mu[i * primes[j]] = -mu[i];
}
}
for (int i = 1; i <= n; i++) sum_mu[i] = sum_mu[i - 1] + mu[i];
}
// 2. 核心函数:利用整除分块计算
ll solve(int n, int m) {
ll ans = 0;
int limit = min(n, m);
// 这里的 l 和 r 就是块的左右边界
for (int l = 1, r; l <= limit; l = r + 1) {
r = min(limit, min(n / (n / l), m / (m / l)));
// 这一块内的 (n/l) 和 (m/l) 都是固定的,直接批量计算
ans += (sum_mu[r] - sum_mu[l - 1]) * (ll)(n / l) * (m / l);
}
return ans;
}
为什么这比神经网络还“艺术”?
- 消除冗余: 暴力法像是一个笨拙的初学者,一遍又一遍地做重复计算。
- 整除分块: 就像是神经网络里的 Pooling(池化) 或者 Batching(批处理)。它意识到一大堆连续的输入产生的输出是完全一样的,所以它索性把它们打包成一个“块”,一次性处理掉。
你的下一个挑战
莫比乌斯反演只是“数论变换”的冰山一角。既然你觉得它像傅里叶变换,那么你一定会对 FWT(快速沃尔什变换) 或者 NTT(快速数论变换) 感兴趣,那些才是真正把“数论”和“信号处理”完美融合的黑科技。
你想了解一下如何通过类似的逻辑,去解决那种“带权限/限制”的求和问题吗?(比如:只求 是质数的情况)
既然你已经掌握了最基础的 (互质计数),我们现在把难度拔高一点,看看莫比乌斯反演如何处理带限制条件的求和。
在 OI 竞赛中,一个非常经典的变化是:求 且 为质数的对数。
1. 问题的变形(手术切入点)
我们要算的式子是:
$$\sum_{p \in \text{prime}} \sum_{i=1}^n \sum_{j=1}^m [\gcd(i, j) = p] $$第一步,按照老规矩,我们把 提出来,同时把 缩小 倍:
$$\sum_{p \in \text{prime}} \sum_{i=1}^{\lfloor n/p \rfloor} \sum_{j=1}^{\lfloor m/p \rfloor} [\gcd(i, j) = 1] $$你看,里面那个双重求和是不是眼熟极了?它就是我们上一节课刚推导过的“互质对数”问题!
2. 引入狄利克雷卷积(多维特征提取)
如果我们把里面的互质对数用莫比乌斯反演替换掉,整个式子会变成:
$$\sum_{p \in \text{prime}} \sum_{d=1}^{\min(\lfloor n/p \rfloor, \lfloor m/p \rfloor)} \mu(d) \lfloor \frac{n}{pd} \rfloor \lfloor \frac{m}{pd} \rfloor $$这时候,为了让计算机跑得更快,我们要用一个常用的技巧:令 。 这样我们可以枚举 ,然后看哪些 和 能乘出 。
最终式子变为:
$$\sum_{T=1}^{\min(n, m)} \left( \sum_{p|T, p \in \text{prime}} \mu(\frac{T}{p}) \right) \lfloor \frac{n}{T} \rfloor \lfloor \frac{m}{T} \rfloor $$括号里那一串 就是我们要预处理的新特征。
3. C++ 实现:线性筛的“威力加强版”
在处理这种题时,我们不只是筛出 ,还要利用线性筛的性质,顺便把那个复杂的括号里的值(我们叫它 )也给预处理出来。
const int MAXN = 1000005;
int mu[MAXN], primes[MAXN], cnt;
int g[MAXN]; // 存储 sum_{p|T} mu(T/p)
ll sum_g[MAXN]; // g 的前缀和
bool vis[MAXN];
void precompute(int n) {
mu[1] = 1;
for (int i = 2; i <= n; i++) {
if (!vis[i]) {
primes[++cnt] = i;
mu[i] = -1;
g[i] = 1; // 当 T 为质数 p 时,g(p) = mu(p/p) = mu(1) = 1
}
for (int j = 1; j <= cnt && i * primes[j] <= n; j++) {
vis[i * primes[j]] = true;
if (i % primes[j] == 0) {
mu[i * primes[j]] = 0;
// 这里的 g[i*p] 计算涉及复杂的分类讨论,
// 但在 OI 中,通常直接用一个额外的循环来预处理 g
break;
} else {
mu[i * primes[j]] = -mu[i];
}
}
}
// 暴力预处理 g(T):对于每个质数 p,更新它的倍数 T
for (int j = 1; j <= cnt; j++) {
for (int T = primes[j]; T <= n; T += primes[j]) {
g[T] += mu[T / primes[j]];
}
}
for (int i = 1; i <= n; i++) sum_g[i] = sum_g[i - 1] + g[i];
}
4. 为什么说这像“激活函数的组合”?
- 函数:像是一个基础的“线性整流”,负责正负抵消。
- 函数:就像是把多个神经元的输出(不同质因数的贡献)叠加在一起。
- 前缀和 + 整除分块:就像是最后做了一次 Softmax 或者 Global Average Pooling,把局部的特征汇总成了最终的答案。
总结
莫比乌斯反演之所以强大,是因为它提供了一种在不完全遍历的情况下,利用“数与数之间的包含关系”直接跳跃计算的能力。
在神经网络里,我们通过权重去学习这种关系;在数论里,莫比乌斯通过 告诉了我们这种关系的精确解。
你现在已经站在了数论算法的高地上。如果你还想更进一步,可以去了解 杜教筛 (Du Sieve) 或者 Min_25 筛,那是处理 级别数据的终极武器。
你想让我给你出个小挑战题,验证一下你现在的“内功”吗?
既然你已经掌握了如何用 函数这把“手术刀”去切开 相关的问题,那我们现在就来看看莫比乌斯反演的**“终极形态”**:杜教筛 (Du Sieve)。
如果说莫比乌斯反演是神经网络里的特征提取,那么杜教筛就是模型压缩与加速的极限——它能让你在 的时间内处理 甚至 级别的数据。这在普通线性筛()面前简直是降维打击。
1. 为什么需要杜教筛?
在 OI 比赛中,如果题目给的 范围是 或 ,线性筛()绰绰有余。 但如果 ,你的内存根本开不下那么大的数组,时间也等不起。
这时候,我们需要一种**“以大化小”**的递归策略。
2. 核心公式:卷积的妙用
假设我们要算莫比乌斯函数的前缀和 。 杜教筛的核心构造式如下:
$$g(1)S(n) = \sum_{i=1}^n (f * g)(i) - \sum_{d=2}^n g(d) S(\lfloor \frac{n}{d} \rfloor) $$这个公式看起来很吓人,但它的逻辑其实非常简洁:
- 左边:我们要算的答案 。
- 右边第一项:如果我们要找一个函数 ,使得 的前缀和非常容易计算(比如恒等于 )。
- 右边第二项:递归项。由于 的取值只有 种,我们可以用整除分块加速。
对于 函数,我们知道 (其中 是元函数,只有在 时为 ,其余为 )。 所以把 带入:
$$S(n) = 1 - \sum_{d=2}^n S(\lfloor \frac{n}{d} \rfloor) $$3. C++ 实现:记忆化搜索 + 哈希表
由于 太大,我们不能用数组存 ,通常使用 std::unordered_map 或者手写哈希表来做记忆化搜索。同时,为了加速,我们会先用线性筛预处理出前 左右的值。
#include <iostream>
#include <unordered_map>
using namespace std;
typedef long long ll;
const int MAXN = 2000000; // 预处理范围
int mu[MAXN], primes[MAXN], cnt;
ll sum_mu[MAXN];
bool vis[MAXN];
unordered_map<int, ll> ans_mu; // 存储大数值的 S(n)
// 1. 线性筛预处理小范围
void init(int n) {
mu[1] = 1;
for (int i = 2; i <= n; i++) {
if (!vis[i]) { primes[++cnt] = i; mu[i] = -1; }
for (int j = 1; j <= cnt && i * primes[j] <= n; j++) {
vis[i * primes[j]] = true;
if (i % primes[j] == 0) break;
mu[i * primes[j]] = -mu[i];
}
}
for (int i = 1; i <= n; i++) sum_mu[i] = sum_mu[i - 1] + mu[i];
}
// 2. 杜教筛核心:递归计算
ll get_sum_mu(int n) {
if (n < MAXN) return sum_mu[n]; // 小范围直接返回
if (ans_mu.count(n)) return ans_mu[n]; // 记忆化
ll ans = 1; // 公式中的第一项 sum (mu*1) = 1
for (int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
// 递归求解:ans -= (g的前缀和) * S(n/d)
ans -= (ll)(r - l + 1) * get_sum_mu(n / l);
}
return ans_mu[n] = ans;
}
4. 深度思考:数学里的“压缩感知”
你之前说它像傅里叶变换,其实杜教筛完美体现了信息压缩的思想:
- 线性筛是“逐位扫描”,虽然稳,但效率受限于数据总量。
- 杜教筛是“结构扫描”。它利用了数论函数在卷积下的自相似性(Self-similarity),通过递归把大问题拆成几个关键的小节点。
这就像现在的 LLM(大语言模型):它不是背下了所有的句子,而是学习了词与词之间的概率分布结构,从而能用有限的参数处理无穷的文本。
终极挑战题
现在你手里已经有了两把神兵利器:
- 莫比乌斯反演(处理逻辑转换)
- 杜教筛(处理超大规模计算)
来试试这道题:
求 ,其中 。
解题思路提示:
- 先用莫比乌斯反演把式子化简为关于 (欧拉函数)或者 的求和式。
- 发现 无法线性筛。
- 利用杜教筛计算 或 的前缀和。
- 配合整除分块出答案。
你想看这个终极组合拳的完整推导吗?还是想自己先在脑子里“跑一编”逻辑?
我们要解决的是 级别的 。这个推导过程就像是把一台复杂的发动机拆解成最基础的零件,然后再用最高效的方式组装回去。
第一步:利用性质“拆解”
数论中有一个非常漂亮的性质:。 也就是说,任何一个数都可以表示为它所有因数的欧拉函数值之和。
我们将这个性质代入 :
$$\sum_{i=1}^n \sum_{j=1}^n \sum_{d|\gcd(i, j)} \phi(d) $$第二步:交换求和顺序(核心手术)
现在我们要改变视角:不再盯着每一对 看,而是盯着每一个可能的因数 看。 问:对于一个确定的 ,有多少对 满足 ?
只要 是 的倍数,且 也是 的倍数, 就会出现在 的因数名单里。 在 到 之间, 的倍数一共有 个。 所以, 有 种选法, 也有 种选法。
于是式子变成了:
$$\sum_{d=1}^n \phi(d) \cdot \lfloor \frac{n}{d} \rfloor^2 $$第三步:解决 的计算压力
观察这个式子:。
- 整除分块:由于 在一段区间内是常数,我们可以用整除分块把它分成 个块。
- 前缀和难题:在每个块里,我们需要求欧拉函数的前缀和 。
- 杜教筛出场:因为 太大,无法线性筛出所有的 ,我们必须用杜教筛。
第四步:推导 的杜教筛公式
利用狄利克雷卷积 (其中 )。 根据杜教筛通用模板:
$$S_{\phi}(n) = \sum_{i=1}^n ID(i) - \sum_{d=2}^n 1 \cdot S_{\phi}(\lfloor \frac{n}{d} \rfloor) $$简化后:
$$S_{\phi}(n) = \frac{n(n+1)}{2} - \sum_{d=2}^n S_{\phi}(\lfloor \frac{n}{d} \rfloor) $$这个式子非常优美:左边是我们要的答案,右边第一项是简单的等差数列求和,第二项是递归。
终极 C++ 组合拳代码结构
这就是 OI 赛场上的满分逻辑:
#include <iostream>
#include <unordered_map>
using namespace std;
typedef long long ll;
const int MAXN = 2000000;
int phi[MAXN], primes[MAXN], cnt;
ll sum_phi[MAXN];
bool vis[MAXN];
unordered_map<int, ll> ans_phi;
// 1. 线性筛预处理小范围 phi
void init(int n) {
phi[1] = 1;
for (int i = 2; i <= n; i++) {
if (!vis[i]) { primes[++cnt] = i; phi[i] = i - 1; }
for (int j = 1; j <= cnt && i * primes[j] <= n; j++) {
vis[i * primes[j]] = true;
if (i % primes[j] == 0) {
phi[i * primes[j]] = phi[i] * primes[j];
break;
}
phi[i * primes[j]] = phi[i] * (primes[j] - 1);
}
}
for (int i = 1; i <= n; i++) sum_phi[i] = sum_phi[i - 1] + phi[i];
}
// 2. 杜教筛求大范围 phi 前缀和
ll get_sum_phi(int n) {
if (n < MAXN) return sum_phi[n];
if (ans_phi.count(n)) return ans_phi[n];
ll ans = (ll)n * (n + 1) / 2;
for (int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l);
ans -= (ll)(r - l + 1) * get_sum_phi(n / l);
}
return ans_phi[n] = ans;
}
// 3. 主函数执行整除分块
int main() {
int n; cin >> n;
init(MAXN - 1);
ll final_ans = 0;
for (int l = 1, r; l <= n; l = r + 1) {
r = n / (n / l);
final_ans += (get_sum_phi(r) - get_sum_phi(l - 1)) * (ll)(n / l) * (n / l);
}
cout << final_ans << endl;
return 0;
}
总结:你的内功进阶路径
- 第一阶段:明白反演是“差分”的变体。
- 第二阶段:学会用 或 的性质替换 。
- 第三阶段:掌握交换求和顺序和整除分块。
- 第四阶段:通过杜教筛突破数据范围限制。
你现在已经站在了数论求和问题的塔尖。这套“杜教筛 + 莫比乌斯反演 + 整除分块”的组合拳,足以解决绝大多数竞赛中的高难度数论题。
你觉得在这一连串的推导中,哪一个步骤最让你感到“数学的巧妙”?是那个交换求和顺序的瞬间吗?