性函数
一说数论函数, 我个人认为积性函数这个叫法更好
对于一个函数 �(�)f(x), 如果满足对于任意的 $(a, b) | ���(�,�)=1,�∈�,�∈�gcd(a,b)=1,a∈Z,b∈Z, �(��)=�(�)�(�)f(ab)=f(a)f(b), 那么称这个函数为积性函数(数论函数)
特别地, 如果对于所有的 (�,�)∣�∈�,�∈�(a,b)∣a∈Z,b∈Z 都有 �(��)=�(�)�(�)f(ab)=f(a)f(b) 的话, 函数 �f 被称为完全积性函数(完全数论函数)
二者区别在于是否要求两整数互质
常见的积性函数
- 常数函数:1(�)=11(n)=1 (完全积性函数)
- 单位函数:�(�)=[�=1]ε(n)=[n=1][1] (完全积性函数)
- 恒等函数:���(�)=��idk(n)=nk, 这里通常所说的恒等函数当 �k 取 11, 即 ��1id1, 一般记作 ��id (完全积性函数)
- 除数函数:��(�)=∑�∣���σk(n)=d∣n∑dk, 通常我们把 �1(�)σ1(n) 记作 �(�)σ(n) 或者 �(�)d(n), 实际意义是 �n 的因数和
- 欧拉函数:�(�)=∑�=1�[���(�,�)=1]φ(n)=i=1∑n[gcd(i,n)=1], 即统计小于等于 �n 的数中与 �n 互质的数的个数
- 莫比乌斯函数:简单说来: 当 �n 等于 11 时, �(�)=1μ(n)=1; 否则, 当 �n 的素数分解中有非一次的素因子时 �(�)=0μ(n)=0; 否则, 当 �n 有奇数个素因子时, �(�)=−1μ(n)=−1; 否则, �(�)=1μ(n)=1. 形式化地
�(�)={1,�=10,� 有平方素因子(−1)�,�=�1�2⋯��(� 个不同素因子)μ(n)=⎩⎨⎧1,0,(−1)k,n=1n 有平方素因子n=p1p2⋯pk(k 个不同素因子)
推荐读者自己证明所有的积性函数的 积性
狄利克雷卷积
对于积性函数 �f 和 �g, 它们的卷积 �∗�f∗g 满足这样一个关系式:
(�∗�)(�)=∑�∣��(�)�(��)(f∗g)(n)=d∣n∑f(n)g(dn)
这里有一个良好的性质, 因为因数是成对出现的, 所以积性函数卷积显然满足交换律(当然卷积本身就满足交换律), 也就是说我们有
(�∗�)(�)=∑�∣��(�)�(��)=∑�∣��(�)�(��)(f∗g)(n)=d∣n∑f(n)g(dn)=d∣n∑g(n)f(dn)
莫比乌斯反演
莫比乌斯反演就是利用莫比乌斯函数和狄利克雷卷积将一些难以直接计算的量转化为便于计算的量从而降低复杂度
最常用的是下列恒等式
∑�∣��(�)=[�=1]=�(�)d∣n∑μ(d)=[n=1]=ε(n)
Proof
根据狄利克雷卷积, 我们还可以知道 �=1∗�ε=1∗μ
以这一个最常见的莫比乌斯反演作为例子, 读者理解后应当可以推出大多其它的反演式子
求解莫比乌斯函数
根据定义我们显然可以枚举出来质因数然后 check, 是 �(�)O(n) 的, 但当我们要求出很多数的莫比乌斯函数的时候这个复杂度就有些难以接受了
为了解决这个问题, 我们来思考如何确定 �n 的莫比乌斯函数的取值, 因为这是一个积性函数, 且对于一个质数 �p, �(�)μ(p) 一定等于 −1−1. 所以当 �n 为合数时, 我们可以枚举小于 �n 的质数 �p, 然后分为两类:
- �∣�p∣n, 这个时候的 �(��)μ(pn) 一定含有一个 �2p2 的因子项, 所以 �(��)=0μ(pn)=0
- �∤�p∤n, 这个时候 �(��)μ(pn) 相当于在 �n 的基础上添加了一个新的因子 �p, 由积性函数定义, �(��)=�(�)×�(�)μ(pn)=μ(p)×μ(n), 由我们刚才所讲的 �(�)μ(p) 一定等于 −1−1, 我们有 �(��)=−1�(�)=−�(�)μ(pn)=−1μ(n)=−μ(n)
我们发现这个过程很类似于筛法, 所以我们可以使用埃氏筛 �(� �� �� �)O(n ln ln n) 的做
但事实上还可以优化, 如果我们使用线性筛, 则可以做到 �(�)O(n). 简要介绍一下线性筛的思想, 只用一个合数最小的质因子来筛除这个数, 具体可以看代码然后感性理解或者 OI-Wiki
而几乎在所有的本文提到的数论函数中, 线性筛其实是好写于埃氏筛的, 通常表现在于我们只需要找到一个最小的 �n 的质因数�p, 这个目标是与线性筛相同的
这里贴一份代码
// ========= Made By BilllIlllly ========= std::vector<bool> is_prime; std::vector<int> mu; std::vector<int> primes; void sieve(int n) { is_prime.assign(n + 1, true); mu.assign(n + 1, 0); is_prime[1] = false; // 如果你认为 1 是质数, 那你也可以去掉这一行 mu[1] = 1; for (int i = 2; i <= n; ++i) { if (is_prime[i]) { primes.emplace_back(i); mu[i] = -1; } for (int p : primes) { if (i * p > n) break; is_prime[i * p] = false; if (i % p == 0) { mu[i * p] = 0; break; } mu[i * p] = -mu[i]; } } }例题
互质数对个数
这是莫比乌斯反演最经(jian)典(dan)的一道例题, 虽然好像并没有找到被出成题的版本
给定一个正整数 �n, 求 ∑�=1�∑�=1�[���(�,�)=1]i=1∑nj=1∑n[gcd(i,j)=1], 简单来说就是在 [1,�][1,n] 中有多少个互质的整数对
朴素做法
枚举 �,�i,j, 然后求gcd, 是 �(�2����)O(n2logn) 的, 不能接受
反演做法
我们发现 [���(�,�)=1][gcd(i,j)=1] 可以写成 �(���(�,�))ε(gcd(i,j)), 所以原式等于
∑�=1�∑�=1��(���(�,�))i=1∑nj=1∑nε(gcd(i,j))
由莫比乌斯反演, 原式等于
∑�=1�∑�=1�∑�∣���(�,�)�(�)i=1∑nj=1∑nd∣gcd(i,j)∑μ(i)
=∑�=1�∑�=1�∑�∣��∣��(�)=i=1∑nj=1∑nd∣id∣j∑μ(i)
考虑从枚举 (�,�)(i,j) 变为枚举 �d, 原式等于
∑�=1�∑�∣��∑�∣���(�)d=1∑nd∣i∑nd∣j∑nμ(d)
这里需要注意的是, 虽然我们写的是 �∣�,�∣�d∣i,d∣j, 但事实上这两个求和的求和记号分别是 �,�i,j, 简单来说我们实际上实在令 �,�i,j 是 �d 的倍数
这个时候发现原式的 �(�)μ(d) 和 �,�i,j 无关, 所以我们直接交换求和次序, 原式等于
∑�=1��(�)∑�∣��∑�∣��1d=1∑nμ(d)d∣i∑nd∣j∑n1
最后我们考虑这里的后面两个 ∑∑ 怎么处理, 事实上, �,�i,j 的取值数量为 �n 中的 �d 的倍数的数量, 也就是 ⌊��⌋⌊dn⌋, 所以总共有 ⌊��⌋2⌊dn⌋2 个取值, 原式就终于被我们化为了
∑�=1�(�(�)⌊��⌋2)d=1∑n(μ(d)⌊dn⌋2)
我们分析这个式子
- �(�)μ(d) 事实上是可以通过预处理达到 �(1)O(1) 的, 预处理的复杂度最常见的可以线性筛(前文讲过); 或者如果需求更快, 可以用杜教筛; 想要更快, 可以用洲阁筛或者Min-25筛(当然我只会线性筛).
- ⌊��⌋2⌊dn⌋2 显然可以 �(1)O(1) 每次的求出
综上, 通过线性筛预处理, 我们至少可以做到 �(�)O(n).
优化:整除分块
现在具体再来讲一下莫比乌斯反演最严厉的父亲——整除分块。
预处理出 �μ 的前缀和,利用整除分块(也叫数论分块),我们可以把单次查询的复杂度降到 �(�)O(n)!
观察式子 ⌊��⌋⌊dn⌋,当 �d 在 [1,�][1,n] 内变化时,这个式子的取值其实只有 �(�)O(n) 种。
为什么呢?
- 当 �≤�d≤n 时,⌊��⌋⌊dn⌋ 显然最多只有 �n 种取值。
- 当 �>�d>n 时,⌊��⌋≤�⌊dn⌋≤n,所以也最多只有 �n 种取值。
综上,取值总数不超过 2�2n。而且,对于同一个取值 �=⌊��⌋k=⌊dn⌋,满足条件的 �d 一定是一段连续的区间 [�,�][l,r]。
这个区间的右端点 �r 怎么求呢?
由 �=⌊��⌋≤��k=⌊dn⌋≤dn,可得 �≤��d≤kn,所以最大的 �d 就是 ⌊��⌋⌊kn⌋。
也就是说,右端点 �=⌊�⌊�/�⌋⌋r=⌊⌊n/l⌋n⌋。
这样,我们就可以把 �d 分成 �(�)O(n) 段,每一段内的 ⌊��⌋2⌊dn⌋2 是相同的,提出来之后,只需要乘上这一段 �(�)μ(d) 的和(即前缀和之差)即可。
预处理前缀和 �(�)O(n),单次查询 �(�)O(n)。我们将一些问题的复杂度从最初的 �(�2����)O(n2logn) 降为了 �(�)O(n)!
YY的GCD
有了整除分块,我们来看一道真正的板子题。
题目大意:给定 �,�N,M,求 ∑�=1�∑�=1�[gcd(�,�)∈prime]i=1∑Nj=1∑M[gcd(i,j)∈prime]。多组询问。
推导过程
首先,枚举质数 �p:
∑�∈prime∑�=1�∑�=1�[gcd(�,�)=�]p∈prime∑i=1∑Nj=1∑M[gcd(i,j)=p]
把 �p 除进去:
=∑�∈prime∑�=1⌊�/�⌋∑�=1⌊�/�⌋[gcd(�,�)=1]=p∈prime∑i=1∑⌊N/p⌋j=1∑⌊M/p⌋[gcd(i,j)=1]
使用基本反演公式 [gcd(�,�)=1]=∑�∣gcd(�,�)�(�)[gcd(i,j)=1]=∑d∣gcd(i,j)μ(d):
=∑�∈prime∑�=1⌊�/�⌋∑�=1⌊�/�⌋∑�∣gcd(�,�)�(�)=p∈prime∑i=1∑⌊N/p⌋j=1∑⌊M/p⌋d∣gcd(i,j)∑μ(d)
把 �d 提出来先枚举:
=∑�∈prime∑�=1min(⌊�/�⌋,⌊�/�⌋)�(�)⌊���⌋⌊���⌋=p∈prime∑d=1∑min(⌊N/p⌋,⌊M/p⌋)μ(d)⌊pdN⌋⌊pdM⌋
这里令 �=��T=pd,则 �=�/�d=T/p。我们改变枚举顺序,先枚举 �T:
=∑�=1min(�,�)⌊��⌋⌊��⌋∑�∣�,�∈prime�(��)=T=1∑min(N,M)⌊TN⌋⌊TM⌋p∣T,p∈prime∑μ(pT)
令 �(�)=∑�∣�,�∈prime�(��)f(T)=p∣T,p∈prime∑μ(pT)。
那么原式 = ∑�=1min(�,�)⌊��⌋⌊��⌋�(�)T=1∑min(N,M)⌊TN⌋⌊TM⌋f(T)
对于 �(�)f(T),我们可以用类似埃氏筛的方法在 �(�log�)O(NlogN) 内预处理出来,并求出前缀和 �(�)=∑�=1��(�)S(T)=∑i=1Tf(i)。
cpp
// 预处理 f(T) for (int i = 1; i <= MAXN; ++i) { if (is_prime[i]) { for (int j = 1; i * j <= MAXN; ++j) { f[i * j] += mu[j]; } } } // 求前缀和 for (int i = 1; i <= MAXN; ++i) { sum_f[i] = sum_f[i - 1] + f[i]; }然后对于每次询问,利用整除分块,可以在 �(�+�)O(N+M) 的时间内求出答案:
cpp
long long solve(int n, int m) { long long res = 0; int limit = min(n, m); for (int l = 1, r; l <= limit; l = r + 1) { r = min(n / (n / l), m / (m / l)); res += (long long)(n / l) * (m / l) * (sum_f[r] - sum_f[l - 1]); } return res; }预处理 �(�log�)O(NlogN),单次询问 �(�)O(N),这题就愉快地解决了。
NDPC_L - LCM
现在回顾这道让我学这个东西的题
题意
给定 �N 和 1…�1…N 的一个排列 �A。
有一个 �N 个顶点的有向图,对于 1≤�<�≤�1≤i<j≤N,从顶点 �i 到顶点 �j 有 lcm(��,��)lcm(Ai,Aj) 条有向边。
求对于每个 �=2,3,…,�v=2,3,…,N,从顶点 1 到顶点 �v 的路径数,对 998244353 取模。两条路径不同当且仅当它们经过的边的集合不同。
�≤2×105N≤2×105。
做法
首先,最直接的思路肯定是 DP(废话这都是ndpc了)。
设 ���dpj 为从顶点 1 到顶点 �j 的路径数。显然 ��1=1dp1=1。
对于 �>1j>1,转移方程为:
���=∑�=1�−1���×lcm(��,��)dpj=i=1∑j−1dpi×lcm(Ai,Aj)
这坨直接算是 �(�2)O(N2) 的,对于 �=2×105N=2×105 显然你 T 飞了。考虑优化。
看到 lcmlcm,显然要把它转化为 gcdgcd:
lcm(�,�)=��gcd(�,�)lcm(x,y)=gcd(x,y)xy
代入转移方程:
���=∑�=1�−1�������gcd(��,��)=��∑�=1�−1�����1gcd(��,��)dpj=i=1∑j−1dpigcd(Ai,Aj)AiAj=Aji=1∑j−1dpiAigcd(Ai,Aj)1
现在问题变成了如何快速处理 1gcd(�,�)gcd(x,y)1。
我们知道,对于任意积性函数 �f,都可以写成 �(gcd(�,�))=∑�∣gcd(�,�)�(�)f(gcd(x,y))=∑d∣gcd(x,y)g(d) 的形式,其中 �=�∗�g=f∗μ。
这里 �(�)=1�f(n)=n1,我们来求一下对应的 �(�)g(d)(为了方便,下面记作 ℎ(�)h(d)):
ℎ(�)=∑�∣��(�)�(��)=∑�∣�1��(��)h(d)=k∣d∑f(k)μ(kd)=k∣d∑k1μ(kd)
因为 �f 和 �μ 都是积性函数,所以 ℎh 也是积性函数。我们只需要算它在质数幂 ��pa 处的值:
ℎ(��)=∑�=0�1���(��−�)h(pa)=i=0∑api1μ(pa−i)
因为 �(��−�)μ(pa−i) 只有在 �−�=0a−i=0 或 11 时非零,所以只有最后两项:
ℎ(��)=1���(1)+1��−1�(�)=1��−1��−1=1−���h(pa)=pa1μ(1)+pa−11μ(p)=pa1−pa−11=pa1−p
所以对于任意 �=∏����n=∏piai:
ℎ(�)=∏1−������=1�∏�∣�(1−�)h(n)=∏piai1−pi=n1p∣n∏(1−p)
于是我们得到了一个非常漂亮的恒等式:
1gcd(�,�)=∑�∣gcd(�,�)ℎ(�)gcd(x,y)1=d∣gcd(x,y)∑h(d)
将这个恒等式代回 ���dpj 的式子中:
���=��∑�=1�−1�����∑�∣gcd(��,��)ℎ(�)dpj=Aji=1∑j−1dpiAid∣gcd(Ai,Aj)∑h(d)
交换求和顺序,先枚举 �d。因为 �d 必须同时整除 ��Ai 和 ��Aj,所以 �d 必须是 ��Aj 的约数:
���=��∑�∣��ℎ(�)∑�=1�−1�����[�∣��]dpj=Ajd∣Aj∑h(d)i=1∑j−1dpiAi[d∣Ai]
令 ��=∑�=1�−1�����[�∣��]Sd=∑i=1j−1dpiAi[d∣Ai],表示前面所有满足 �∣��d∣Ai 的 �����dpiAi 的和。
那么转移方程就变成了:
���=��∑�∣��ℎ(�)��dpj=Ajd∣Aj∑h(d)Sd
在计算出 ���dpj 之后,我们需要更新所有的 ��Sd。对于 ��Aj 的每一个约数 �d,我们将 �����dpjAj 加到 ��Sd 中:
��←��+�����Sd←Sd+dpjAj
复杂度
这个算法的复杂度如何呢?
对于每个 �j,我们需要枚举 ��Aj 的所有约数。
因为 �A 是 1…�1…N 的排列,所以 ��≤�Aj≤N。
一个数 �≤�x≤N 的约数个数平均是 �(log�)O(logN)。
所以枚举约数的总复杂度是 ∑�=1��(��)=∑�=1��(�)=�(�log�)∑j=1Nd(Aj)=∑x=1Nd(x)=O(NlogN)。
预处理 ℎ(�)h(d) 和每个数的列表,也显然可以在 �(�log�)O(NlogN) 的复杂度内完成
所以总时间复杂度 �(�log�)O(NlogN),空间复杂度 �(�log�)O(NlogN)
代码实现
这里贴一份核心代码:
// divs[x] 存储了 x 的所有约数 // h[d] 就是 h(d) // S[d] 维护 S_d static constexpr int MOD = 998244353 std::vector<long long> dp(N + 1, 0); std::vector<long long> S(N + 1, 0); dp[1] = 1; // 初始化 S,把 dp[1] * A[1] 加到 A[1] 的所有约数对应的 S 中 long long val1 = A[1]; // dp[1] * A[1] for (int d : divs[A[1]]) S[d] = (S[d] + val1) % MOD; for (int j = 2; j <= N; ++j) { long long sum = 0; // 计算 dp[j] for (int d : divs[A[j]]) sum = (sum + S[d] % MOD * h[d]) % MOD; dp[j] = sum * A[j] % MOD; // 输出答案 std::cout << dp[j] << '\n'; // 更新 S,为了后面的 j 转移 if (j < N) { long long val = dp[j] * A[j] % MOD; for (int d : divs[A[j]]) S[d] = (S[d] + val) % MOD; }