狄利克雷卷积 - 入门
前情提要:学弟说想学莫反,所以先教大家怎么玩乘法卷积。
这是第二篇教程,如果不知道什么是反演、高维前缀和,请先阅读这个 差分与前缀和。
我们会先说狄利克雷卷积,然后讲一点题目,把狄利克雷生成函数 DGF 留给下一篇。
求莫比乌斯函数
根据定义,可以花费 \(O(\sqrt n)\) 时间完成质因子分解,求出单个 \(\mu(n)\) 的值。
同时,由于是积性函数,我们可以在 \(O(n)\) 时间里用线性筛求出 \(1..n\) 的值。
mu[1] = 1;
for (int x = 2; x <= n; x++) {
if (!vis[x]) { // 是质数
primes.push_back(x);
mu[x] = -1;
}
for (int p : primes) {
if (x * p > n) break;
vis[x * p] = 1;
if (x % p == 0) { // x 里有 p
mu[x * p] = 0; // xp 肯定有 p2
break;
} else { // x 里没有 p
mu[x * p] = -mu[x]; // xp 恰有一个 p
}
}
}
一般线性筛法
考虑有一个未知的积性函数 \(f\),假设已经得知形如 \(f(p^k)\) 处的值,就可以在线性时间内求解每个点值。
否则的话,还需要额外算上单独为 \(\frac{n}{\ln n}\) 个质数求解每个 \(f(p^k)\) 的开销。
low[N]; // i 的最小质因子的最大幂次 p^c 的值
f[1] = 1; // 积性函数 h(1) 总是 1
for (int x = 2; x <= n; x++) {
if (!vis[x]) { // 是质数
primes.push_back(x);
low[x] = x;
f[x] = /* f(p) */;
}
for (int p : primes) {
if (x * p > n) break;
vis[x * p] = 1;
if (x % p == 0) { // x 里有 p
low[x * p] = low[x] * p;
if (low[x] == x) f[x * p] = /* f(p^{k+1}) */;
else f[x * p] = f[x / low[x]] * f[low[x * p]]; // 拆成互质的两部分
break;
} else { // x 里没有 p
low[x * p] = p;
f[x * p] = f[x] * f[p];
}
}
}
完全积性函数
完全积性函数 \(f(n)\) 满足:\(f(1) = 1\),对任意 \(a,b\),都满足 \(f(ab) = f(a)f(b)\)。
一些例子是 \(\textbf{1}(n)\),\(e(n)=[n=1]\),\(\text{id}^k(n) = n^k\),\(\lambda(n) = (-1)^{\Omega(n)}\),这里说下最后一个:
- \(\Omega(n)\) 表示 \(n\) 的可重素因子个数。比如 \(12 = 2^2 \cdot 3^1\),于是有 \(\Omega(12) = 3\)。
- 由于素因子个数在乘法中是相加的,即 \(\Omega(ab) = \Omega(a) + \Omega(b)\),因此 \(\lambda(ab) = (-1)^{\Omega(a)+\Omega(b)} = \lambda(a)\lambda(b)\)。
不用太刻意去记,总是可以当场考察是否满足。
更多!乘法卷积
给定两个数论函数 \(f\) 和 \(g\),它们的狄利克雷卷积定义为:$$(f * g)(n) = \sum_{d\vert{}n} f(d) g\left(\frac{n}{d}\right)$$
我们之前提到,乘法卷积就是加法卷积在高维空间的推广,因此也满足:交换律,结合律,分配律,具有单位元。
乘法卷积还满足积性封闭的性质:如果 \(f,g\) 是积性函数,那么 \(f * g\) 也是积性函数,故可以对后者使用线性筛法。
如果 \(f * g = e\),那么我们称 \(g\) 是 \(f\) 的狄利克雷逆元,也记作 \(f^{-1}\)。逆元存在的充要条件是 \(f(1) \ne 0\)。
当 \(n = 1\) 时:
当 \(n > 1\) 时,此时 \((f * f^{-1})(n) = e(n) = 0\),让我们展开卷积:
移项即可得逆元公式。这两种写法是等价的,但不用背,可以现场推:
如果 \(f\) 没有任何特殊性质,可以在 \(O(n \log n)\) 时间内求解 \(g = f^{-1}\):
g[1] = inv(f[1]);
// 考虑 (2) 的形式,这里 i 表示 n/d,j 表示 d,i*j 表示 n
for (int i = 1; i <= n; i++) {
if (i > 1) g[i] *= -g[1]; // 算完 g[i]
for (int j = 2; i * j <= n; j++) // 把 g[i] 的贡献加出去
g[i * j] += g[i] * f[j];
}
狄利克雷逆元,以及完全积性函数具有一些很牛的性质:
- 积性封闭:如果 \(f\) 是积性函数,那么它的逆元 \(f^{-1}\) 也是积性函数。
也就是说,这种情况下,我们对 \(f^{-1}\) 也可以使用线性筛法。 - 完全积性函数的逆元:如果 \(f\) 是完全积性函数,则 \(f^{-1}(n) = \mu(n) f(n)\)。
注意右边不是卷积,只是表示第 \(i\) 项相乘。这里简单证明这一性质:
- 完全积性函数的分配律:如果 \(\alpha\) 是完全积性函数,则 \((\alpha \cdot f) * (\alpha \cdot g) = \alpha \cdot (f * g)\)
- 完全积性函数与逆元:如果 \(\alpha\) 是完全积性函数,则 \((\alpha f)^{-1} = \alpha (f^{-1})\)
后两条性质可以用和前面类似的推倒方式,后两条对 \(f,g\) 不要求积性。
注意!积性封闭不代表完全积性封闭。完全积性函数的逆元,一定是积性函数,但不一定是完全积性!考虑 \(\textbf{1}\) 和 \(\mu\) 的情形。
相关筛法
因为是积性封闭的,所以如果 \(f,g\) 都是积性函数,那么 \(h = f * g\) 也是积性函数,可以 \(O(n)\) 时间里筛出:
rem[N]; // i 除掉所有 p 余下来的部分
lpf[N]; // 最小质因子 p
h[1] = 1; // 积性函数 f(1) 总是 1
for (int x = 2; x <= n; x++) {
if (!vis[x]) { // 是质数
primes.push_back(x);
rem[i] = 1;
lpf[x] = x;
}
for (int p : primes) {
if (x * p > n) break;
vis[x * p] = 1;
rem[x * p] = x % p ? x : rem[x];
lpf[x * p] = p;
if (x % p == 0) break;
}
if (rem[x] == 1) { // 说明是 p 的幂次,暴力枚举求卷积
for (int k = x; k; k /= lpf[x])
h[x] += f[k] * g[x / k];
} else { // 拆成互质的两部分
h[x] = h[rem[x]] * h[x / rem[x]];
}
}
类似的,也可以在线性时间里求出积性函数 \(f\) 的逆 \(f^{-1}\):
// for (int p : primes) { ... }
if (rem[x] == 1) { // 说明是 p 的幂次,暴力枚举求卷积
// inv[x] = -\sum_{d|x, d>1} f[d] * inv[x/d]
for (int d = lpf[x]; d <= x; d *= lpf[x])
inv[x] -= f[d] * inv[x / d];
} else { // 拆成互质的两部分
inv[x] = inv[rem[x]] * inv[x / rem[x]];
}
练习 - 完全积性函数
前缀和 1
给定 \(n\) 个正整数 \(a_i\),问有多少四元对满足 \(\gcd(a_{i_1}, a_{i_2}, a_{i_3}, a_{i_4}) = x\),对 \(x = 1 \dots n\) 回答。
\(1 \le n, a_i \le 10^6\)
设 \(f(x)\) 表示满足恰 \(\gcd(a_{i_1}, a_{i_2}, a_{i_3}, a_{i_4}) = x\) 的四元对数量;
设 \(F(x)\) 表示 \(\gcd(a_{i_1}, a_{i_2}, a_{i_3}, a_{i_4})\) 为 \(x\) 的倍数的四元对数量。
根据我们所设的定义,恰好符合莫反的倍数形式,可以这样描述 \(f\) 和 \(F\) 的关系:
我们记 \(S(x)\) 表示有多少 \(a_i\) 是 \(x\) 的倍数,这个可以在 \(O(n \log n)\) 时间里暴力求出:
我们可以用组合数求出每个 \(F(x)\),即从合法的 \(a_i\) 里选出 \(4\) 个来:
随后反解出 \(f\) 的值:
这样的时间复杂度为 \(O(n \log n)\),已经非常优秀。可以优化到 \(O(n \log \log n)\)。
我们的瓶颈在于暴力枚举计算 \(S\) 和 \(f\),两者复杂度都是调和级数的 \(O(n \log n)\)。
观察 \(S(x)\) 是狄利克雷后缀和 \(\text{cnt} * \textbf{1}\),而 \(f = \mu * F\) 则是后缀差分。可以用埃氏筛求解,就做到了 \(O(n \log \log n)\)。
for (int p : primes) // 狄利克雷后缀和,枚举每个质数维度
for (int j = V / p; j >= 1; j--) // 从大到小
cnt[j] += cnt[j * p];
for (int k = 1; k <= V; k++) // 求出 F
F[k] = C(cnt[k], 4);
for (int p : primes) // 狄利克雷后缀差分,枚举每个质数维度
for (int j = 1; j <= V / p; j++) // 从小到大
F[j] -= F[j * p];
也就是说,如果满足以下条件,就可以从 \(O(n \log n)\) 优化到 \(O(n \log \log n)\):
- 卷上了一个完全积性函数,如 \(\textbf{1}\);
- 卷上了一个完全积性函数的狄利克雷逆,如 \(\mu\)。
注意完全积性函数的狄利克雷逆,一定是积性的,但不一定是完全积性的。
我们会在后文严格推导证明,并给出更通用的描述,读者可以先把这个作为结论。
前缀和 2
给定 \(f\) 的前 \(n\) 项,计算 \(h(n) = \sum_{d \mid n} f(d) \frac{n}{d}\) 的前 \(n\) 项。
\(1 \le n \le 10^6\)
也就是说,希望你求 \(h = f * \text{id}\) 的前 \(n\) 项,直接调和级数地,枚举倍数即可做到 \(O(n \log n)\)。
但因为 \(\text{id}\) 是完全积性函数,我们考虑狄利克雷前缀和,对于质数维度 \(p\),转移系数 \(\text{id}(p) = p\):
for (int p : primes) // 前缀和,即 * id
for (int j = 1; j * p <= n; j++)
f[j * p] += f[j] * p;
for (int p : primes) // 差分,即 * id^-1(本题没用到)
for (int j = n / p; j >= 1; j--)
f[j * p] -= f[j] * p;
复杂度为 \(O(n \log \log n)\)。对于任何完全积性函数,只需要将这里的系数 \(p\) 改为 \(g(p)\) 的值即可。
前缀和 3
给定 \(f\) 的前 \(n\) 项,计算 \(h(n) = \sum_{d \mid n} f(d) \varphi(\frac{n}{d})\) 的前 \(n\) 项。
\(1 \le n \le 10^6\)
即求卷积 \(f * \varphi\),考虑欧拉反演 \(\textbf{1} * \varphi = \text{id}\),我们可以改写所求 \(f * \text{id} * \mu\) 形式。
因为 \(\text{id}\) 是完全积性的,可以先以 \(\text{id}\) 作为系数进行前缀和,然后再进行无系数的差分。
for (int p : primes) // 先前缀和,即 * id
for (int j = 1; j * p <= n; j++)
f[j * p] += f[j] * p;
for (int p : primes) // 再差分,即 * mu
for (int j = n / p; j >= 1; j--)
f[j * p] -= f[j];
这样是 \(O(n \log\log n)\) 的。如果直接卷 \(\varphi\) 的话,由于不是完全积性,只能做到 \(O(n \log n)\)。
练习:Dirichlet 半在线卷积
已知函数 \(f\) 满足 \(f(1)=1\),且
给定正整数 \(n\),试求出 \(f(1),f(2),\cdots,f(n)\) 的值。模 \(2^{32}\)自然溢出。
\(1 \le n \le 5 \times 10^7\)。
由于 \(f\) 之间相互依赖,我们必须半在线地求解,一般的手段是 CDQ 分治。
然而这个题目非常神秘,它帮你把 \(d=n\) 抠掉了,让我们考察发生了什么:
- 任何数 \(x\) 的真因子 \(d\) 满足 \(d \le x/2\)。
- 如果已经求出了 \([1, \text{mid}]\) 的所有 \(f\) 值,那么 \([\text{mid}, n]\) 中所有数的真因子 \(f\) 值都已经算完了。
也就是说为了算 \([1,n]\) 的 \(f\),我们只要在算完 \([1, \text{mid}]\) 的部分以后就可以开始卷了。
void solve(int n) {
if (n == 1) return void(f[1] = 1);
int mid = n / 2;
solve(mid); // 算 [1, mid] 的 f
// 把左半区间的 f 值复制到辅助数组,右半部分补 0
for (int i = 1; i <= mid; i++) A[i] = f[i];
for (int i = mid + 1; i <= n; i++) A[i] = 0;
for (int p : primes) { // 卷 id
if (p > n) break;
for (int i = 1; i * p <= n; i++) A[i * p] += A[i] * p;
}
for (int p : primes) { // 卷 mu
if (p > n) break;
for (int i = n / p; i >= 1; i--) A[i * p] -= A[i];
}
// 这里 A[i] 即为 f * phi 的结果,赋给右半区间的 f 即可
for (int i = mid + 1; i <= n; i++) f[i] = A[i];
}
时间是 \(T(n) = T(n/2) + O(n \log\log n) = \Theta(n \log\log n)\),可以把单侧递归改成倍增来减少常数。
练习 - 积性函数
前面我们说,如果卷了完全积性函数,或者完全积性函数的逆,可以在 \(O(n \log \log n)\) 时间内求解:
- 完全积性函数是正着遍历
f[j * p] += f[j] * g(p) - 卷上它的逆是倒着遍历
f[j * p] -= f[j] * g(p)
重要的是,这个过程可以被理解为是在若干条极长的 \(j\),\(jp\),\(jp^2\),\(\dots jp^k\) (\(j \perp p\)) 的不交链上做卷积。
具体来说,我们找到每个 \(j \perp p\),取出所有的 \(jp^k\),设链上元素 \(f\) 值分别是 \(A_0,A_1 \dots A_L\),然后计算:
都计算完成后把 \(f \leftarrow A'(x)\) 赋值回去,枚举下一个质数维度 \(p\) 计算。而当 \(g\) 是完全积性,有 \(g(p^c) = g(p)^c\),此时发现:
恰好变成了 \(O(L)\) 的带 \(g(p)\) 作为系数的“前缀和”,也就完成了证明。而对于仅积性函数的情况,我们可以 \(O(L^2)\) 转移。
可以证明如果 \(g(p^c)\) 可以快速计算(或已经预处理),即使是 \(O(L^2)\) 暴力在链上卷积,复杂度依然是 \(O(n \log \log n)\) 的。
Proofs.
我们先固定一个 \(p\),设 \(N_k = \frac{n}{p^k}\),此时长度恰好为 \(L\) 的链的数量为 \(N_{L-1} - N_{L}\)。
这是因为对于一条有 \(L\) 个元素的极长链,满足 \(jp^{L-1} \le n\) 而 \(jp^{L} \gt n\),使得 \(j\) 的数量相减即为所求。
此时对于我们固定的 \(p\),其总计算次数如下:
最后一步只是暴力展开了前面的 \(N\),括号里的 \(\sum_{k=1}^{\infty} \frac{k+2}{p^k}\) 用比值审敛法可知 \(|p|>1\) 时绝对收敛。总复杂度:
练习:很多前缀和
给定 \(f\) 的前 \(n\) 项,计算 \(f * \underbrace{\mathbf{1} * \mathbf{1} * \cdots * \mathbf{1}}_{k \text{ 次}}\) 的前 \(n\) 项,模大质数 \(998244353\)。
\(1 \le n \le 10^6\),\(1 \le k \le 2^{1000}\)
不考虑函数 \(g = \textbf{1}\) 的任何性质,由于乘法卷积具有结合律,一种通用的做法是快速幂:
我们不断计算 \(g^{2^h}\) 随后和 \(f\) 卷起来,复杂度为 \(O(n \log n \log k)\),但数据范围有点大。
事实上,\(\textbf{1}^{*k}\) 的结果是 \(k\) 阶除数函数 \(d_k(n)\),即把 \(n\) 拆分成 \(k\) 个正整数有序乘积的方案数。
首先它是积性函数,所以可以线性筛求 \(d_k\) 然后暴力 \(O(n \log n)\) 卷一下,不过我们能做的更好。
考察质数的幂次 \(p^a\),我们用插板法,其公式为:
这里的 \(a\) 不超过 \(\log n\),只要暴力预处理这些组合数即可,考虑组合数的具体展开:
因为 \(a < P\),分母总是存在逆元,可以对分子上的 \(k\) 模 \(P\),这样就解决了数据范围。
接下来考虑转移,因为是完全积性函数,所以可以按质数 \(p\) 逐维度转移:
- 考虑每次总是在 \(j\),\(j p\),\(jp^2 \dots\) 共 \(L\) 项之间转移,即在这条链上做 \(k\) 次前缀和;
- 第 \(t = 0 \dots x\) 项,在完成前缀和后,对第 \(x\) 项的贡献如下,可以 \(O(L^2)\) 求解。
for (int p : primes) {
for (int j = 1; j <= n / p; j++) {
// 找链的头部 j(不是 p 的倍数)
if (j % p == 0) continue;
ll now = j;
vector<ll> A; // 找出这条链:j, jp, jp^2, ...
while (now <= n) A.push_back(f[now]), now *= p;
vector<ll> res(A.size(), 0);
for (ll x = 0; x < A.size(); x++) { // 在链上转移
ll sum = 0;
for (ll t = 0; t <= x; t++) sum = (sum + A[x - t] * C[t]) % MOD;
res[x] = sum;
}
now = j; // 把结果写回 f
for (ll i = 0; i < A.size(); i++) f[now] = res[i], now *= p;
}
}
可以证明复杂度依然是 \(O(n \log\log n)\),一份常数比较小的代码如下
auto solve(const std::vector<int>& f, const std::vector<int>& g) {
int n = f.size() - 1;
auto h = f;
for (int p : primes) {
for (int k = n / p * p; k; k -= p) {
int d = k;
while (true) {
d /= p;
h[k] += h[d] * g[k / d]; // k / d 依次取到 p^1, p^2 ...
if (d % p) break;
}
}
}
return h;
}
相信读者通过这部分的题目,对乘法卷积,以及一些套路有了更加深刻的认识!

浙公网安备 33010602011771号