狄利克雷卷积 - 入门

前情提要:学弟说想学莫反,所以先教大家怎么玩乘法卷积。

这是第二篇教程,如果不知道什么是反演、高维前缀和,请先阅读这个 差分与前缀和。
我们会先说狄利克雷卷积,然后讲一点题目,把狄利克雷生成函数 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\) 时:

\[f^{-1}(1) = \frac{1}{f(1)} \]

当 \(n > 1\) 时,此时 \((f * f^{-1})(n) = e(n) = 0\),让我们展开卷积:

\[\begin{align*} \sum_{d \mid n} f\left(\frac{n}{d}\right) f^{-1}\left(d \right) &= 0 \\ \sum_{d \mid n, d \ne n} f\left(\frac{n}{d}\right) f^{-1}\left(d \right) &= -f(1)f^{-1}(n) \\ \end{align*} \]

移项即可得逆元公式。这两种写法是等价的,但不用背,可以现场推:

\[\begin{align} f^{-1}(n) &= -\frac{1}{f(1)} \sum_{d\vert{}n, d \ne n} f\left(\frac{n}{d}\right) f^{-1}(d) \\ f^{-1}(n) &= -\frac{1}{f(1)} \sum_{d\vert{}n, d \ne 1} f(d) f^{-1}\left( \frac{n}{d}\right) \end{align} \]

如果 \(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\) 项相乘。这里简单证明这一性质:

\[(f \cdot \mu) * f(n) = \sum_{d\vert{}n} f(d)\mu(d)f(\frac{n}{d}) = \sum_{d\vert{}n} f(n)\mu(d) = f(n)\sum_{d\vert{}n}\mu(d) = f(n)e(n) = e(n) \]

  • 完全积性函数的分配律:如果 \(\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\) 的关系:

\[F(x) = \sum_{k = 1} f(kx) \]

我们记 \(S(x)\) 表示有多少 \(a_i\) 是 \(x\) 的倍数,这个可以在 \(O(n \log n)\) 时间里暴力求出:

\[S(x) = \sum_{k=1} \text{cnt}(kx) \]

我们可以用组合数求出每个 \(F(x)\),即从合法的 \(a_i\) 里选出 \(4\) 个来:

\[F(x) = \binom{S(x)}{4} \]

随后反解出 \(f\) 的值:

\[f(x) = \sum_{k = 1} \mu(k) F(kx) \]

这样的时间复杂度为 \(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\),且

\[f(n)=\sum_{d|n,d \ne n}f(d)\varphi(\frac{n}{d}). \]

给定正整数 \(n\),试求出 \(f(1),f(2),\cdots,f(n)\) 的值。模 \(2^{32}\)自然溢出。
\(1 \le n \le 5 \times 10^7\)。

link

由于 \(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\),然后计算:

\[A'(x) = \sum_{c = 0}^L A(x - c) \cdot g(p^c) \]

都计算完成后把 \(f \leftarrow A'(x)\) 赋值回去,枚举下一个质数维度 \(p\) 计算。而当 \(g\) 是完全积性,有 \(g(p^c) = g(p)^c\),此时发现:

\[\begin{align*} A'(x) &= A(x) \cdot {g(p)}^0 + A(x-1) \cdot {g(p)}^1 \dots + A(0) \cdot {g(p)}^x \\ A'(x-1) &= A(x-1) \cdot {g(p)}^0 + A(x-2) \cdot {g(p)}^1 \dots + A(0) \cdot {g(p)}^{x-1} \end{align*} \]

恰好变成了 \(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\),其总计算次数如下:

\[\begin{align*} W(p) &= \sum_{L=2}^n \frac{L(L+1)}{2} (N_{L-1} - N_{L}) \\ &= 3(N_1 - N_2) + 6(N_2 - N_3) + 10(N_3 - N_4) + 15(N_4 - N_5) + \dots \\ &= 3N_1 + \sum_{k=2}^{n} (k+1) N_k \\ &\le \frac{n}{p} \left( 3 + \frac{3}{p} + \frac{4}{p^2} + \frac{5}{p^3} + \dots \right) \\ \end{align*} \]

最后一步只是暴力展开了前面的 \(N\),括号里的 \(\sum_{k=1}^{\infty} \frac{k+2}{p^k}\) 用比值审敛法可知 \(|p|>1\) 时绝对收敛。总复杂度:

\[T = \sum_{p \le n} W(p) \le \sum_{p \le n} O\left( \frac{n}{p} \right) = O(n) \cdot \sum_{p \le n} \frac{1}{p} = O(n \log\log n) \]

练习:很多前缀和

给定 \(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\),我们用插板法,其公式为:

\[d_k(p^a) = \binom{a + k - 1}{a} \]

这里的 \(a\) 不超过 \(\log n\),只要暴力预处理这些组合数即可,考虑组合数的具体展开:

\[\frac{1}{a!} \times k \times (k+1) \times \dots \times (k+a-1) \]

因为 \(a < P\),分母总是存在逆元,可以对分子上的 \(k\) 模 \(P\),这样就解决了数据范围。

接下来考虑转移,因为是完全积性函数,所以可以按质数 \(p\) 逐维度转移:

  • 考虑每次总是在 \(j\),\(j p\),\(jp^2 \dots\) 共 \(L\) 项之间转移,即在这条链上做 \(k\) 次前缀和;
  • 第 \(t = 0 \dots x\) 项,在完成前缀和后,对第 \(x\) 项的贡献如下,可以 \(O(L^2)\) 求解。

\[A(x) = \sum_{t = 0}^n A(x - t) \times \binom{t + k - 1}{t} \]

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

相信读者通过这部分的题目,对乘法卷积,以及一些套路有了更加深刻的认识!

posted @ 2026-09-29 16:59  Aurora5090  阅读(22)  评论(0)    收藏  举报