跳转至

莫比乌斯反演

前置知识:数论分块狄利克雷卷积

莫比乌斯反演是数论中的重要内容.对于一些函数 f(n),如果很难直接求出它的值,而容易求出其倍数和或约数和 g(n),那么可以通过莫比乌斯反演简化运算,求得 f(n) 的值.

莫比乌斯函数

莫比乌斯函数(Möbius 函数)定义为

μ(n)={1,n=1,0,n is divisible by a square >1,(1)k,n is the product of k distinct primes.

具体地,假设正整数 n 有素因数分解 n=i=1kpiei,其中,pi 是素数,ei 是正整数.那么,三种情形分别对应:

  1. μ(1)=1
  2. 当存在 i 使得 ei>1,即存在任何素因数出现超过一次时,μ(n)=0
  3. 否则,对于所有 i 都有 ei=1,即任何素因数都只出现一次时,μ(n)=(1)k,其中,k 就是互异素因子的个数.

性质

根据定义容易验证,莫比乌斯函数 μ(n) 是积性函数,但不是完全积性函数.除此之外,最为重要的性质是下述恒等式:

性质

对于正整数 n,有

dnμ(d)=[n=1]={1,n=1,0,n1.

其中 [] 是 Iverson 括号.

证明

n=i=1kpiei,设 n=i=1kpi.根据 二项式定理,有

dnμ(d)=dnμ(d)=i=0k(ki)(1)i=(1+(1))k=[k=0]=[n=1].

利用 Dirichlet 卷积,该表达式可以写作 ε=1μ.也就是说,莫比乌斯函数是常值函数 1 的 Dirichlet 逆.

这一性质有一个很常见的应用:

[ij]=[gcd(i,j)=1]=dgcd(i,j)μ(d)=d[di][dj]μ(d).

它将互素的条件转化为关于莫比乌斯函数的求和式,方便进一步推导.

求法

如果需要对单个 n 计算莫比乌斯函数 μ(n) 的值,可以利用它的 质因数分解.例如,在 n 不太大时,可以在 O(n) 时间内求出 μ(n) 的值.

参考实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int mu(int n) {
  int res = 1;
  for (int i = 2; i * i <= n; ++i) {
    if (n % i == 0) {
      n /= i;
      // Check if square-free.
      if (n % i == 0) return 0;
      res = -res;
    }
  }
  // The remaining factor must be prime.
  if (n > 1) res = -res;
  return res;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def mu(n):
    res = 1
    i = 2
    while i * i <= n:
        if n % i == 0:
            n //= i
            # Check if square-free
            if n % i == 0:
                return 0
            res = -res
        i += 1
    # The remaining factor must be prime
    if n > 1:
        res = -res
    return res

如果需要对前 n 个正整数预处理出 μ(n) 的值,可以利用它是积性函数,通过 线性筛O(n) 时间内计算.

参考实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
std::vector<int> get_mu(int n) {
  std::vector<int> mu(n + 1), primes;
  std::vector<bool> not_prime(n + 1);
  primes.reserve(n);
  mu[1] = 1;
  for (int x = 2; x <= n; ++x) {
    if (!not_prime[x]) {
      primes.push_back(x);
      mu[x] = -1;
    }
    for (int p : primes) {
      if (x * p > n) break;
      not_prime[x * p] = true;
      if (x % p == 0) {
        mu[x * p] = 0;
        break;
      } else {
        mu[x * p] = -mu[x];
      }
    }
  }
  return mu;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def get_mu(n):
    mu = [0] * (n + 1)
    primes = []
    not_prime = [False] * (n + 1)

    mu[1] = 1
    for x in range(2, n + 1):
        if not not_prime[x]:
            primes.append(x)
            mu[x] = -1
        for p in primes:
            if x * p > n:
                break
            not_prime[x * p] = True
            if x % p == 0:
                mu[x * p] = 0
                break
            else:
                mu[x * p] = -mu[x]
    return mu

莫比乌斯反演

莫比乌斯函数最重要的应用就是莫比乌斯反演.

莫比乌斯反演

f(n),g(n) 是两个数论函数.那么,有

f(n)=dng(d)g(n)=dnμ(nd)f(d).
证明一

直接验证,有:

dnμ(nd)f(d)=dnμ(nd)kdg(k)=kng(k)d[kdn]μ(nd)=kng(k)dn[ndnk]μ(nd)=kng(k)[nk=1]=g(n).

式子变形的关键在于交换求和次序,并注意到 kdn 就等价于 ndnk.倒数第二个等号相当于对 nk 的因子 nd 处的莫比乌斯函数求和,所以就等于 [nk=1].这一表达式仅在 n=k 处不是 0,最后就会得到 g(n)

证明二

利用 Dirichlet 卷积,命题等价于

f=1gg=μf.

利用 1μ=ε,在左式的等号两侧同时对 μ 做卷积,就得到

fμ=(1g)μ=(1μ)g=εg=g.

在涉及各种整除关系的数论函数求和中,莫比乌斯反演是有力的变形工具.

例子
  1. 欧拉函数 φ(n) 满足关系式 n=dnφ(d),亦即 id=1φ.对它进行反演,就得到 φ=μid,亦即

    φ(n)=dndμ(nd).
  2. 除数函数 σk(n)=dndk,亦即 σk=1idk.对它进行反演,就得到 idk=μσk,亦即

    nk=dnμ(nd)σk(d).
  3. 互异素因子数目函数 ω(n)=dn[dP],亦即 ω=11P,其中 1P 是素数集 P 的指示函数.对它进行反演,就得到 1P=μω,亦即

    [nP]=dnμ(nd)ω(d).
  4. 考察满足 logn=dnΛ(d) 的数论函数 Λ(n).它就是对数函数的莫比乌斯反演,也称为 von Mangoldt 函数:

    Λ(n)=dnμ(nd)logd={logp,n=pe, pP, eN+,0,otherwise.
附:Λ(n) 表达式的证明

对于素数幂 n=pe (eN+),有

Λ(n)=i=0eμ(pei)logpi=logpelogpe1=logp.

对于 n=1,显然有 Λ(n)=log1=0.对于其他合数 n,有

Λ(n)=dnμ(d)(lognlogd)=(dnμ(d))logndnμ(d)logd.

根据莫比乌斯函数的性质,logn 一项的系数为 [n=1]=0.对于后面的一项,可以进一步将 d 分解为素因数之积.对于任何素数 pn,考察 logp 的系数,都有:

pdnμ(d)=(d/p)(n/p)μ(dp)=[np=1]=0.

由此,对于不止一个素因子的合数 n,都有 Λ(n)=0

拓展形式

除了上述基本形式外,莫比乌斯反演还有一些常见的拓展形式.首先,可以考虑它的倍数和形式.

拓展一

f(n),g(n) 是两个数论函数.那么,有

f(n)=ndg(d)g(n)=ndμ(dn)f(d).
证明

直接验证,有:

ndμ(dn)f(d)=ndμ(dn)dkg(k)=nkg(k)d[ndk]μ(dn)=nkg(k)nd[dnkn]μ(dn)=nkg(k)[kn=1]=g(n).

这和基本形式的推导完全对偶.

其次,莫比乌斯反演并不仅限于加法,它实际上对于任何 Abel 群 中的运算都成立.例如,它有如下的乘法形式:

拓展二

f(n),g(n) 是两个数论函数.那么,有

f(n)=dng(d)g(n)=dnf(d)μ(n/d).
证明

直接验证,有:

dnf(d)μ(n/d)=dn(kdg(k))μ(n/d)=kng(k)(d[kdn]μ(nd))=kng(k)(dn[ndnk]μ(nd))=kng(k)[nk=1]=g(n).

其中,ab=ab 是 Knuth 箭头.对比基本形式的证明可以发现,唯一的区别就是加法换成了乘法,且乘法换成了取幂.

从 Dirichlet 卷积的角度看,莫比乌斯反演只是利用了「莫比乌斯函数是常值函数的 Dirichlet 逆」这一点.容易想象,类似莫比乌斯反演的关系对于一般的 Dirichlet 逆同样成立.

拓展三

f(n),g(n),α(n) 都是数论函数,且 α1(n)α(n) 的 Dirichlet 逆,即

[n=1]=dnα(nd)α1(d).

那么,有

f(n)=dnα(nd)g(d)g(n)=dnα1(nd)f(d).
证明

直接验证,有:

dnα1(nd)f(d)=dnα1(nd)kdα(dk)g(k)=kng(k)d[kdn]α(dk)α1(nd)=kng(k)dn[ndnk]α(dk)α1(n/kd/k)=kng(k)[nk=1]=g(n).

和基本形式的证明相比较,只需要将倒数第二个等号替换成 Dirichlet 逆的定义式.

推论

f(n),g(n) 是数论函数,且 t(n) 是完全积性函数.那么,有

f(n)=dnt(nd)g(d)g(n)=dnμ(nd)t(nd)f(d).
证明

只需要验证对于完全积性函数 t(n),它的 Dirichlet 逆就是 μ(n)t(n).直接验证,有:

dnμ(d)t(d)t(nd)=dnμ(d)t(n)=t(n)[n=1]=[n=1].

最后一步用到了 t(1)=1.这对于 完全积性函数 是成立的.

最后,莫比乌斯反演还可以推广到 [1,+) 上的复值函数,而不仅仅局限于数论函数.基本形式的莫比乌斯反演可以看作是复值函数在所有非整数点处均取零值的特殊情形.

拓展四

F(x)G(x) 都是 [1,+) 上的复值函数.那么,有

F(x)=n=1xG(xn)G(x)=n=1xμ(n)F(xn).
证明

不妨对 FG 补充定义,设当 x<1 时,恒有 F(x)=G(x)=0.那么,命题就等价于:

F(x)=nG(xn)G(x)=nμ(n)F(xn).

这些求和式都是对 nN+ 求和.

直接验证,有:

nμ(n)F(xn)=nμ(n)dG(x/nd)=kG(xk)nkμ(n)=kG(xk)[k=1]=G(x).

其中,为得到第二个等号,需要令 k=nd

推论

f(n),g(n) 是数论函数.那么,有

f(n)=k=1ng(nk)g(n)=k=1nμ(k)f(nk).
证明

只需要取 F(x)=f(x)G(x)=g(x) 即可.

这些拓展形式之间可以互相组合,进而得到更为复杂的反演关系.

Dirichlet 前缀和

前置知识:前缀和与差分

考虑基本形式的莫比乌斯反演关系:

f(n)=dng(d)g(n)=dnμ(nd)f(d).

左侧等式中,f(n) 的值是 n 的所有因数处 g(n) 的值之和.如果将 ab 理解为 a 排在 b 之前,那么 f(n) 就可以理解为某种意义下 g(n) 的前缀和.因此,在国内竞赛圈,由 {g(k)}k=1n 求出 {f(k)}k=1n 的过程也称为 Dirichlet 前缀和,相应的逆过程则称为 Dirichlet 差分.这些方法大多出现在需要预处理某个数论函数在前 N 个点处取值的情形.

接下来,讨论 Dirichlet 前缀和的计算.如果将每一个素数都看作一个维度,这就是一种高维前缀和.回忆高维前缀和的 逐维前缀和算法:逐个遍历所有的维度,并将每个位置的值都累加到该位置在该维度上的后继位置.对于数论函数,这相当于说,从小到大遍历所有素数 p,并将 n 处的函数值累加到 np 处.这和 Eratosthenes 筛法 的遍历顺序是一致的.因此,这一算法可以在 O(nloglogn) 时间内计算出长度为 n 的数列的 Dirichlet 前缀和.类似地,利用逐维差分就可以在相同时间复杂度内求出数列的 Dirichlet 差分.

参考实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
std::vector<int> dirichlet_presum(const std::vector<int>& g) {
  int n = g.size() - 1;
  std::vector<int> f(g);
  std::vector<bool> vis(n + 1);
  for (int x = 2; x <= n; ++x) {
    if (vis[x]) continue;
    for (int y = 1, xy = x; xy <= n; ++y, xy += x) {
      vis[xy] = true;
      f[xy] += f[y];
    }
  }
  return f;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
std::vector<int> dirichlet_diff(const std::vector<int>& f) {
  int n = f.size() - 1;
  std::vector<int> g(f);
  std::vector<int> vis(n + 1);
  for (int x = 2; x <= n; ++x) {
    if (vis[x]) continue;
    for (int y = n / x, xy = x * y; y; --y, xy -= x) {
      vis[xy] = true;
      g[xy] -= g[y];
    }
  }
  return g;
}

这一计算方法可以推广到倍数和(拓展一)、乘积形式(拓展二)、利用完全积性函数代替常值函数(拓展三的推论)等拓展形式中.

例题

本节通过例题展示莫比乌斯反演的应用方法以及一些常见变形技巧.首先,通过一道例题熟悉处理求和式中最大公因数条件的基本技巧.

Luogu P2522 [HAOI 2011] Problem b

T 组数据.对每组数据,求值:

i=xnj=ym[gcd(i,j)=k].

数据范围:1T,x,y,n,m,k5×104

解答

根据容斥原理,原式可以分成 4 块来处理,且每一块的式子都具有形式

f(n,m,k)=i=1nj=1m[gcd(i,j)=k].

对于这类式子,接下来是一段标准的推导流程:提取公因数,应用莫比乌斯函数性质,交换求和次序.

首先,由于 i,j 都只能取 k 的倍数,可以先将这个因子提出来——这相当于代入 i=kij=kj,就得到:

f(n,m,k)=i=1n/kj=1m/k[gcd(i,j)=1].

再利用莫比乌斯函数的性质可知:

[gcd(i,j)=1]=dgcd(i,j)μ(d)=d[di][dj]μ(d).

将它代入表达式,并交换求和次序,就得到:

f(n,m,k)=dμ(d)(i=1n/k[di])(j=1m/k[dj]).

这样一段操作的好处是,固定 d 时,求和式中关于 ij 的项相互分离,可以分别求和.接下来,因为

i=1n/k[di]=n/kd, j=1m/k[dj]=m/kd,

所以,有

f(n,m,k)=dμ(d)n/kdm/kd.

用线性筛预处理完 μ(d),并预处理其前缀和后,就可以通过数论分块求解.总的时间复杂度为 O(N+TN),其中,Nn,m 的上界,T 为数据组数.

参考代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <algorithm>
#include <iostream>
using namespace std;
constexpr int N = 50000;
int mu[N + 5], p[N + 5];
bool flg[N + 5];

void init() {
  int tot = 0;
  mu[1] = 1;
  for (int i = 2; i <= N; ++i) {
    if (!flg[i]) {
      p[++tot] = i;
      mu[i] = -1;
    }
    for (int j = 1; j <= tot && i * p[j] <= N; ++j) {
      flg[i * p[j]] = true;
      if (i % p[j] == 0) {
        mu[i * p[j]] = 0;
        break;
      }
      mu[i * p[j]] = -mu[i];
    }
  }
  for (int i = 1; i <= N; ++i) mu[i] += mu[i - 1];
}

int solve(int n, int m) {
  int res = 0;
  for (int i = 1, j; i <= min(n, m); i = j + 1) {
    j = min(n / (n / i), m / (m / i));
    res += (mu[j] - mu[i - 1]) * (n / i) * (m / i);  // 代推出来的式子
  }
  return res;
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  int T, a, b, c, d, k;
  init();  // 预处理mu数组
  cin >> T;
  for (int i = 1; i <= T; i++) {
    cin >> a >> b >> c >> d >> k;
    // 根据容斥原理,1<=x<=b&&1<=y<=d范围中的答案数减去1<=x<=b&&1<=y<=c-1范围中的答案数和
    //   1<=x<=a-1&&1<=y<=d范围中的答案数再加上1<=x<=a-1&&1<=y<=c-1范围中的答案数
    //   即可得到a<=x<=b&&c<=y<=d范围中的答案数
    // 这一步如果不懂可以画坐标图进行理解
    cout << solve(b / k, d / k) - solve(b / k, (c - 1) / k) -
                solve((a - 1) / k, d / k) + solve((a - 1) / k, (c - 1) / k)
         << '\n';
  }
  return 0;
}

接下来的两道例题展示了枚举公因数的处理方法,并利用 筛法 计算一般积性函数的值.

SPOJ LCMSUM

T 组数据.对每组数据,求值:

i=1nlcm(i,n).

数据范围:1T3×105, 1n106

解答一

题目提供的是最小公倍数,但往往最大公因数更容易处理.所以,首先做变形:

f(n)=i=1nlcm(i,n)=i=1ningcd(i,n).

n 提出,并枚举最大公因数 k

f(n)=nkni=1nik[gcd(i,n)=k].

对于内层的求和式,这是最常见的含有最大公因数的情形,重复标准的处理流程,就有:

f(n)=nkni=1n/ki[gcd(i,nk)=1]=nkni=1n/kidμ(d)[di][dnk]=nkndμ(d)[dnk](i=1n/ki[di]).

再次地,关于 i 的求和式与其他部分分离,可以单独处理.最后的求和式实际上是一个等差数列求和:(取 i=di

i=1n/ki[di]=d12(nkd+1)nkd=:dG(nkd).

由此,就得到如下表达式:

f(n)=nkndμ(d)[dnk]dG(nkd).

在枚举公因数之后,这样形式的二重求和式很常见.对于它,同样有固定的处理方法:将乘积设为新变量 =kd,然后再次交换求和次序.因为 d(n/k) 就相当于 dn,所以,原式变形为:

f(n)=nnG(n)dμ(d)d.

F()=dμ(d)d,则原式具有形式:

f(n)=nnG(n)F().

因为 μ(d)d 是积性函数,所以它和常值函数 1 的卷积 F(n) 也是积性函数.尽管上述表达式中,求和式呈现 Dirichlet 卷积的形式,但是 G(n) 并非积性函数,所以这一求和式的整体并非积性函数.但是,G(n) 是多项式,所以它其实是若干完全积性函数的线性组合.所以,有

f(n)=12n((n)2F()+nF()).

这两项(不包含系数)都是积性函数,可以直接通过线性筛预处理(或者也可以线性筛出内层函数后,用 Dirichlet 前缀和在 O(NloglogN) 时间内预处理).具体地,设

Hs(n)=(n)sF(), s=1,2.

要推导它们的表达式,只需要确定它们在素数幂处的取值即可.为此,对于素数 p 和正指数 e,有

F(pe)=μ(1)+μ(p)p+j=2eμ(pj)pj=1p,Hs(pe)=(pe)sF(1)+j=1e(pej)sF(pj)=pes+(1p)1pes1ps, s=1,2.

特别地,H1(pe)1 是常值函数,而

H2(pe)=p2e+(1p)1p2e1p2=H2(pe1)+p2ep2e1.

这就很容易通过线性筛求解.在线性筛预处理出 H2(n) 后,单次询问可以通过表达式 f(n)=(n/2)(H2(n)+1)O(1) 时间内求解.总的时间复杂度为 O(N+T),其中,Nn 的上界,T 为数据组数.

参考实现中,利用本题表达式的特殊性,对线性筛部分做了进一步推导,这并非必须的.仅利用素数幂处的取值,仍然可以在 O(N) 时间内完成预处理.这些推导详见解答二.

解答二

就本题而言,有着更为灵活的处理方法.从解答一可以看出

f(n)=nkni=1n/ki[gcd(i,nk)=1]=nknF(nk).

如果在这一步不继续做莫比乌斯反演,而是观察后面的求和式实际上是不超过 d=n/k 且与之互素的整数之和.对于 d>1,因为与 d 互素的整数成对出现,即 idi 必定同时与 d 互素,所以,有

F(d)=i=1ni[id]=i=1d(di)[id]=12di=1d[id]=12dφ(d).

对于 d=1,则有

F(d)=1=12+12dφ(d).

进而,原式可以表示为

f(n)=12n(dndφ(d)+1).

由于 G(n)=dndφ(d) 是积性函数 nφ(n) 与常值函数 1 的 Dirichlet 卷积,所以它也是积性函数,可以通过线性筛预处理.为此,只需要确定它在素数幂处的取值.对于素数 p 和正指数 e,有

G(pe)=1+i=1epe(pe1)=G(pe1)+p2ep2e1.

可以看出,这一表达式和解答一推导的结果是一致的.这一方法的总时间复杂度仍然是 O(N+T)

最后,利用本题积性函数的表达式,可以进一步优化线性筛的计算过程.对于素数 p,有

G(p)=1p+p2.

线性筛的关键在于对于一般的 n,需要求出 G(pn) 的取值.这进一步分为两种情形.当 pn 时,因为 G 是积性函数,所以

G(pn)=G(p)G(n).

否则,当 pn 时,设 n=pempm,就有

G(pn)=G(pe+1)G(m)=G(pe)G(m)+(p2e+2p2e+1)G(m)=G(n)+(p2e+2p2e+1)G(m).

直接验证可知,这一表达式对于 pn 的情形也成立.因此,就有

G(n)G(np)=(p2ep2e1)G(m).

代入上式,就得到

G(pn)=G(n)+p2(G(n)G(np)).

这简化了线性筛部分的计算.当然,这一推导并非必需,对于没有特殊性质的积性函数,直接利用 G(pn)=G(pe+1)G(m) 就可以完成线性筛的计算.

参考代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
constexpr int N = 1000000;
int tot, p[N + 5];
long long g[N + 5];
bool flg[N + 5];  // 标记数组

void solve() {
  g[1] = 1;
  for (int i = 2; i <= N; ++i) {
    if (!flg[i]) {
      p[++tot] = i;
      g[i] = (long long)1 * i * (i - 1) + 1;
    }
    for (int j = 1; j <= tot && i * p[j] <= N; ++j) {
      flg[i * p[j]] = true;
      if (i % p[j] == 0) {
        g[i * p[j]] =
            g[i] + (g[i] - g[i / p[j]]) * p[j] * p[j];  // 代入推出来的式子
        break;
      }
      g[i * p[j]] = g[i] * g[p[j]];
    }
  }
}

using std::cin;
using std::cout;

int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  int T, n;
  solve();  // 预处理g数组
  cin >> T;
  for (int i = 1; i <= T; ++i) {
    cin >> n;
    cout << (g[n] + 1) * n / 2 << '\n';
  }
  return 0;
}
BZOJ 2154 [国家集训队] Crash 的数字表格

求值:

i=1nj=1mlcm(i,j)mod20101009.

数据范围:1n,m107

解答

推导过程中忽略模数.设

f(n,m)=i=1nj=1mlcm(i,j).

依然是将最小公倍数转换为最大公因数,枚举公因数,并应用标准的处理流程,就得到

f(n,m)=ki=1nj=1mijk[gcd(i,j)=k]=ki=1n/kj=1m/kkij[gcd(i,j)=1]=ki=1n/kj=1m/kkijdμ(d)[di][dj]=kkdμ(d)(i=1n/ki[di])(j=1m/kj[dj]).

再次地,求和式对于 ij 分离.首先计算这些内层的求和式,提取因数(即取 i=di),就有

i=1n/ki[di]=di=1n/k/di=dG(n/kd)=dG(nkd).

其中,G(n)=12n(n+1) 就是等差数列求和,最后一个等号利用了下取整函数的 特性.对称地,对于另一个和式可以类似计算.代回前文表达式,就有

f(n,m)=kkdμ(d)d2G(nkd)G(mkd).

和前文的情形一致,对于这类枚举公因数的式子,往往都需要枚举乘积 =kd,再次交换求和次序:

f(n,m)=(dμ(d)d)G(n)G(m).

F()=dμ(d)d.

这是积性函数 与积性函数 dμ(d)d 的乘积,所以也是积性函数,可以直接用线性筛预处理,并预处理出它的前缀和.然后,就可以用数论分块计算 f(n,m) 的值.总的时间复杂度为 O(min{n,m})

参考代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  constexpr int M = 20101009;
  int n, m;
  std::cin >> n >> m;
  if (n > m) std::swap(n, m);
  std::vector<int> f(n + 1), vis(n + 1), prime;
  prime.reserve(n);
  f[1] = 1;
  for (int x = 2; x <= n; ++x) {
    if (!vis[x]) {
      prime.emplace_back(x);
      f[x] = 1 - x;
    }
    for (int p : prime) {
      if (1LL * p * x > n) break;
      vis[x * p] = 1;
      f[x * p] = x % p ? (1 - p) * f[x] : f[x];
      if (x % p == 0) break;
    }
  }
  for (int x = 1; x <= n; ++x) {
    f[x] = 1LL * x * f[x] % M + M;
    f[x] = (f[x] + f[x - 1]) % M;
  }
  long long res = 0;
  for (int l = 1, r; l <= n; l = r + 1) {
    int nn = n / l, mm = m / l;
    r = std::min(n / nn, m / mm);
    int g_n = (1LL * nn * (nn + 1) / 2) % M;
    int g_m = (1LL * mm * (mm + 1) / 2) % M;
    res += 1LL * g_n * g_m % M * (f[r] - f[l - 1] + M) % M;
  }
  std::cout << (res % M) << '\n';
  return 0;
}

接下来的一道例题较为特殊,需要对乘积的约数个数函数进行转换.

LOJ 2185. [SDOI2015] 约数个数和

T 组数据.对每组数据,求值:

i=1nj=1mσ0(ij).

其中,σ0(n)=dn1 表示 n 的约数个数.

数据范围:1n,m,T5×104

解答

这道题目的难点在于将 σ0(ij) 转换为关于最大公因数的表达式.由于 σ0 是积性函数,可以首先考虑素数幂的情形.对于素数 p 和非负指数 e1,e2,设 i=pe1, j=pe2,就有

σ0(ij)=1+e1+e2=xiyj[xy].

对于一般情形,不妨设 i=pipj=pjp,其中,ip,jp 分别是 i,j 的素因数分解中 p 的幂次.进而,有

σ0(ij)=pσ0(ipjp)=pxpipypjp[xpyp].

注意到,对于 i 的每个素数幂因子 ip 都枚举它的因数 xp,就相当于对 i 枚举它的因数 x 再分解出所有素数幂因子 xp;对 j 同理.因此,利用乘法分配律,该式就有

σ0(ij)=xiyjp[xpyp]=xiyj[xy].

最后一步用到了结论:xy,当且仅当对于每个素因子 p,都有 xpyp 成立.

得到这一表达式后,就可以应用标准的处理流程:

σ0(ij)=xiyj[xy]=xiyjdμ(d)[dx][dy]=dμ(d)(x[dxi])(y[dyj])=dμ(d)[di][dj]σ0(id)σ0(jd).

最后一步推导的含义是:函数只有在 didj 时才取非零值,且此时,枚举满足 dxix 就相当于枚举 id 的因数 xd,枚举满足 dyjy 同理.

将这一表达式再代回原式,并交换求和次序:

f(n,m)=i=1nj=1mσ0(ij)=i=1nj=1mdμ(d)[di][dj]σ0(id)σ0(jd)=dμ(d)(i=1n[di]σ0(id))(j=1m[dj]σ0(jd))=dμ(d)(i=1n/dσ0(i))(j=1m/dσ0(j)).

G(n)=i=1nσ0(i),就有

f(n,m)=dμ(d)G(nd)G(md).

这可以通过数论分块求解.只需要预处理出 μ(n)σ0(n) 的前缀和即可.总时间复杂度为 O(N+TN),其中,Nn,m 的上界,T 为数据组数.

参考代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <algorithm>
#include <iostream>
using namespace std;
constexpr long long N = 5e4 + 5;
long long n, m, T, pr[N], mu[N], d[N], t[N],
    cnt;  // t 表示 i 的最小质因子出现的次数
bool bp[N];

void prime_work(long long k) {
  bp[0] = bp[1] = true, mu[1] = 1, d[1] = 1;
  for (long long i = 2; i <= k; i++) {  // 线性筛
    if (!bp[i]) pr[++cnt] = i, mu[i] = -1, d[i] = 2, t[i] = 1;
    for (long long j = 1; j <= cnt && i * pr[j] <= k; j++) {
      bp[i * pr[j]] = true;
      if (i % pr[j] == 0) {
        mu[i * pr[j]] = 0;
        d[i * pr[j]] = d[i] / (t[i] + 1) * (t[i] + 2);
        t[i * pr[j]] = t[i] + 1;
        break;
      } else {
        mu[i * pr[j]] = -mu[i];
        d[i * pr[j]] = d[i] << 1;
        t[i * pr[j]] = 1;
      }
    }
  }
  for (long long i = 2; i <= k; i++)
    mu[i] += mu[i - 1], d[i] += d[i - 1];  // 求前缀和
}

long long solve() {
  long long res = 0, mxi = min(n, m);
  for (long long i = 1, j; i <= mxi; i = j + 1) {  // 整除分块
    j = min(n / (n / i), m / (m / i));
    res += d[n / i] * d[m / i] * (mu[j] - mu[i - 1]);
  }
  return res;
}

int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  cin >> T;
  prime_work(50000);  // 预处理
  while (T--) {
    cin >> n >> m;
    cout << solve() << '\n';
  }
  return 0;
}

最后一道例题展示了如何应用乘法版本的莫比乌斯反演.

Luogu P5221 Product

求值:

i=1nj=1nlcm(i,j)gcd(i,j)(mod104857601).

数据范围:1n1×106

解答一

推导过程中忽略模数.设

f(n)=i=1nj=1nlcm(i,j)gcd(i,j).

依然是将最小公倍数转换为最大公因数:

f(n)=i=1nj=1nij(gcd(i,j))2.

注意,对这些因子的乘积是相互独立的,可以分别计算.令

g(n)=i=1nj=1ngcd(i,j).

原式就等于:

f(n)=(n!)2ng(n)2.

重点是解决 g(n) 的计算问题.对它的处理流程和前文描述的相仿,但是需要换成相应的乘法版本.首先,枚举并提取公因数:

g(n)=ki=1nj=1nk[gcd(i,j)=k]=ki=1n/kj=1n/kk[gcd(i,j)=1].

其中,ab=ab 是 Knuth 箭头.然后,代入 [gcd(i,j)=1]=dμ(d)[di][dj],并将指数上的和式转换为幂的乘积式,得到:

g(n)=kdi=1n/kj=1n/kk(μ(d)[di][dj]).

进一步地提取因数(即令 i=dij=dj),并应用下取整函数的 特性,就得到:

g(n)=kdi=1n/(kd)j=1n/(kd)kμ(d).

然后分离关于 i,j 的乘积,就发现乘式中并不含有 i,j,因此它就相当于对乘式取幂:

g(n)=kdk(μ(d)nkd2).

因为前面枚举了公因数,所以对于这个式子需要再次交换求乘积的次序.令 =kd,有:

g(n)=d(d)(μ(d)n2)=(d(d)μ(d))n2.

F(n)=dn(nd)μ(d).

容易发现这是关于 F~(n)=n 的乘积形式莫比乌斯反演.即使不知道它的表达式,也可以应用 Dirichlet 差分 方法在 O(nloglogn) 时间内预处理.当然,由于 F~(n) 的形式非常简单,F(n) 的表达式可以直接求出:

F(n)={p,n=pe, pP, eN+,1,otherwise.

von Mangoldt 函数 就是它的自然对数.得到 F(n) 的取值后,直接应用乘积版本的数论分块就可以在 O(n) 时间内求出 g(n) 的取值,进而得到 f(n) 的取值.总的时间复杂度为 O(n)

值得注意的是,涉及乘积的计算时,往往需要用到 欧拉定理,因此指数部分取模用到的模数与题目所给的模数并不相同.

解答二

乘积版本推导的难点在于对乘积和幂次的处理相对陌生,因此,对于这类问题,也可以取对数后再推导.对于本题,仅考虑 g(n) 的推导.将它取对数后,有:

logg(n)=i=1nj=1nloggcd(i,j).

对于这类含有最大公因数的式子,直接应用标准的推导流程,就得到:

logg(n)=klogki=1nj=1n[gcd(i,j)=k]=klogki=1n/kj=1n/k[gcd(i,j)=1]=klogkdμ(d)(i=1n/k[id])(j=1n/k[jd])=klogkdμ(d)nkd2=(dμ(d)logd)n2=Λ()n2.

其中,Λ(n)von Mangoldt 函数.将这一推导结果取幂,就得到解答一的结果.

参考代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <algorithm>
#include <iostream>
#include <vector>

int pow(int a, int b, int m) {
  int res = 1;
  for (int po = a; b; b >>= 1) {
    if (b & 1) res = 1LL * res * po % m;
    po = 1LL * po * po % m;
  }
  return res;
}

int main() {
  constexpr int M = 104857601;
  int n;
  std::cin >> n;
  // Get F(n), i.e., exp(Lambda(n)).
  std::vector<int> vis(n + 1), prime, pf(n + 1), rem(n + 1);
  prime.reserve(n);
  for (int x = 2; x <= n; ++x) {
    if (!vis[x]) {
      prime.emplace_back(x);
      pf[x] = x;
      rem[x] = 1;
    }
    for (int p : prime) {
      if (1LL * p * x > n) break;
      vis[x * p] = 1;
      pf[x * p] = p;
      rem[x * p] = x % p ? x : rem[x];
      if (x % p == 0) break;
    }
  }
  pf[1] = 1;
  for (int x = 2; x <= n; ++x) {
    pf[x] = rem[x] == 1 ? pf[x] : 1;
  }
  // Prefix products and their inverses.
  std::vector<int> pm(n + 1), ip(n + 1);
  pm[0] = 1;
  for (int x = 1; x <= n; ++x) {
    pm[x] = 1LL * pm[x - 1] * pf[x] % M;
  }
  int inv = pow(pm[n], M - 2, M);
  ip[0] = 1;
  for (int x = n; x >= 1; --x) {
    ip[x] = inv;
    inv = 1LL * inv * pf[x] % M;
  }
  // Calculate g(n).
  int res = 1;
  for (int l = 1, r; l <= n; l = r + 1) {
    r = n / (n / l);
    int a = 1LL * pm[r] * ip[l - 1] % M;
    int b = 1LL * (n / l) * (n / l) % (M - 1);
    res = 1LL * res * pow(a, b, M) % M;
  }
  // Take square and then inverse.
  res = pow(res, M - 3, M);
  // Get factorials.
  int fac = 1;
  for (int x = 1; x <= n; ++x) {
    fac = 1LL * fac * x % M;
  }
  // Final answer.
  res = 1LL * res * pow(fac, 2 * n, M) % M;
  std::cout << res << std::endl;
  return 0;
}

习题

参考文献