莫比乌斯反演和数论函数

感觉一直没有认真学过数论,这次好好记一下。

前置知识:整除分块,筛法。

一些符号说明:

[...]:这个是艾弗森括号,意思是若括号内的表达式成立则为 1,否则为 0

有些时候会用 (i,j) 来表示 gcd(i,j)

若非特殊说明,除法均为下取整。

Part 1 基础数论函数 & 狄利克雷卷积

基础数论函数

数论函数指定义域为整数的函数,也可视作一个数列。

一些常用的数论函数:

  • 单位函数:ϵ(n)=[n=1]
  • 恒等函数:idk(n)=nk。特别地,当 k=1 的时候通常简记作 id(n)
  • 常数函数:1(n)=1
  • 除数函数:σk(n)=d|ndk。特别地,当 k=1 的时候通常简记作 σ(n)σ0(n) 通常简记作 d(n)τ(n)
  • 欧拉函数:φ(n)=i=1n[(i,n)=1],即表示 1n 中与 n 互质的数的个数。
  • 莫比乌斯函数:μ(n)={1n=10d>1,d2|n(1)ω(n)otherwise,其中 ω(n) 表示 n 本质不同的质因子个数。

完全积性函数:若函数 f 满足 f(1)=1x,yN 均有 f(xy)=f(x)f(y),则 f 为完全积性函数,如 1(n)ϵ(n)φ(n)μ(n) 都是完全积性函数。

积性函数:若函数 f 满足 f(1)=1x,yN,xy 均有 f(xy)=f(x)f(y),则 f 为积性函数。显然积性函数包含完全积性函数。

积性函数的一个性质:f(n)=f(pixi)。这是因为 pixipjxj。所以一般研究积性函数可以先研究 f(pk) 的值,然后就可以筛了。

狄利克雷(Dirichlet)卷积

定义两个数论函数 f(x),g(x),则它们的狄利克雷卷积得到的结果 h(x) 定义为:h(x)=d|xf(d)g(xd)=ab=xf(a)g(b)。这可以简记为 h=fg

性质:交换律fg=gf),结合律(fg)h=f(gh)),分配律(f+g)h=fh+gh)。

单位元:单位函数 ϵ 是狄利克雷卷积运算中的单位元,即 fϵ=f

逆元:对于任意满足 f(1)0 的数论函数 f,若存在另一个数论函数 g 满足 fg=ϵ,则称 gf 的逆元。显然逆元是唯一的。容易构造出 g(n)g(n)=ϵ(n)d>1,d|nf(d)g(nd)f(1)

重要结论:

两个积性函数的 Dirichlet 卷积也是积性函数

证明:记 h=fg

则根据积性函数的定义,有:ab,h(n)=h(a)h(b)

h(a)=i|af(i)g(ai)h(b) 同理。

则有

h(n)=i|af(i)g(ai)j|bf(j)g(bj)=i|aj|bf(i)f(j)g(ai)g(bj)

因为 ab,所以有 ij,aibj,则可继续化为

h(n)=i|aj|bf(ij)g(abij)=d|abf(d)g(abd)

所以 h(n) 为积性函数,原命题得证。


积性函数的逆元也是积性函数。

证明:由于表示出 g(n) 的式子是一个由大规模到小规模的样子,考虑使用归纳法证明。

fg=ϵ,不妨令 f(1)=1

ab=1,则 g(ab)=g(1)=1,此时结论显然成立。

ab>1(ab),假设当前所有 i<ab 的结论均成立,则有:

g(ab)=d>1,d|abf(d)g(abd)=x|a,y|b,xy>1f(xy)g(abxy)=x|a,y|b,xy>1f(x)f(y)g(ax)g(by)=f(1)g(ab)x|af(x)g(ax)y|bf(y)g(by)=f(1)f(1)g(a)g(b)ϵ(a)ϵ(b)=g(a)g(b)ϵ(a)ϵ(b)=g(a)g(b)

所以 g 为积性函数。


一些常用的 Dirichlet 卷积的例子:

  • ϵ=μ1ϵ(n)=d|nμ(n)
  • d=11d(n)=d|n1
  • σ=id1σ(n)=d|nd
  • φ=μidφ(i)=d|ndμ(nd)

Part 2 莫比乌斯反演

引入

μ 为莫比乌斯函数,定义为:μ(n)={1n=10d>1,d2|n(1)ω(n)otherwise

μ 最开始引入的原因是若 g=d|nf(d),则 g=1f,但是这样只能根据 fg,而从 g 推到 f 的式子显然就变为了 f=11g,然后 Mobius 就将 11 命名为一个新的函数,就是 μ 了。

ϵ 是单位元,则有 1μ=ϵ,我们可以据此对 μ 式子进行对到推导:

过程:

首先 μ(1) 显然为 1

由于 1 是积性函数,所以 μ=11 一定也是积性函数,不妨先考虑 μ(pk)k>0)的值。

  1. k=1,由于 d|p1(d)μ(pd)=ϵ(p)=0,所以 μ(1)+μ(p)=0,可以得到 μ(p)=1
  2. k>1,有 d|pk1(d)μ(pkd)=ϵ(k)=0,所以 i=0kμ(pk)=0,对 k 从小到大考虑并带入 μ(pk) 的值后,易得 μ(pk)=0(k>1)

然后由于 μ 是积性函数,所以所有含有平方或更高次方的因子的时候,均有 μ(n)=0

然后剩下的数肯定就是由若干个本质不同的质数相乘得到的,则 μ(n)=p|n,pprimeμ(p),所以 μ(n)=(1)ω(n)ω(n) 就表示 n 的本质不同的质数个数。

容易发现 μ 的式子就是上面的形式。

ϵ=1μ 可以推出一个很好的结论:d|nμ(d)={1n=10n1,这个结论在下面的例题中会经常用到。

线筛 μ

发现 μ 是一个积性函数,显然可以线筛。这里就直接放代码了,不懂线筛的可以看 这个

Code:

bool vis[500010];
int cnt, p[500010], mu[500010];
void init(int n) {
  mu[1] = 1;
  for(int i = 2; i <= n; ++i) {
    if(!vis[i])
      mu[i] = -1, p[++cnt] = i;
    for(int j = 1; j <= cnt && i * p[j] <= n; ++j) {
      mu[i * p[j]] = i % p[j] == 0 ? 0 : -mu[i];
      vis[i * p[j]] = 1;
      if(i % p[j] == 0)
        break;
    }
  }
}

然后就是介绍怎么反演了。

反演公式

f,g 为数论函数。

第一种:若 f(n)=d|ng(d),则 g(n)=d|nμ(d)f(nd)。证明显然,f=g1g=μf

第二种:若 f(n)=d|ng(d),则 g(n)=d|nμ(nd)f(d),这个就是因为 Dirichlet 卷积具有交换律,即 μf=fμ

莫反的一个重要结论:d|nμ(d)=ϵ(n),当 n>1 的时候式子就为 0 了。证明就是 μ1=ϵ

拓展φ(i)1=id

证明

n=pk 时,

φ1=d|pkφ(d)=p0+p0(p1)+p1(p1)++pk1(p1)=pk=id

然后由于 φ1 是积性函数,所以这里就只证明 pk 的取值满足就行了。

φ1=id 可以得到 idμ=φ

Part 3 一些例题

1 一道经典的题
Problem

给定 n,m,求 i=1nj=1m[gcd(i,j)=1] 的值。

要求在 O(n+T×min(n,m)) 的时间复杂度内解决,T 为测试数据的组数。

Sol

因为 i,j 维交换顺序不影响答案,所以不妨令 nm

[(i,j)=1] 的这个条件显然与 ϵ((i,j)) 等价,使用莫比乌斯变换可以变为 i=1nj=1md|(i,j)μ(d)

然后我们交换求和顺序,变为 d=1nd|ind|jmμ(d),因为 d|(i,j)d|i,d|j。然后发现 μ(d) 可以直接提出,化成:d=1nμ(d)d|ind|jm1,对于每个 dij1 显然为 ni×mi。于是就可以对 μ 求前缀和之后对 nimi 进行整除分块了。

Code

没有。

2 P2522 [HAOI 2011] Problem b](https://www.luogu.com.cn/problem/P2522)
Problem

n 组询问,每组询问包含五个数 a,b,c,d,k,求 i=abj=cd[(i,j)=k]

1n,k5×1041ab5×1041cd5×104

2.5s,250MB

Sol

显然可以先将平面上的矩形查询变通过容斥变为前缀矩形的贡献,不妨令 solve(n,m,k) 表示 i=1nj=1m[(i,j)=k] 的值,显然答案就是 solve(b,d,k)solve(a - 1, d, k)solve(b,c1,k)+solve(a1,c1,k)

考虑如何求解 solve(n,m,k)(这里由于交换 n,m 后的答案不变,不妨令 nm):

i=1nj=1m[(i,j)=k]=i=1n/kj=1m/k[(ik,jk)=k]=i=1n/kj=1m/k[(i,j)=1]

然后由例题一可知 i=1n/kj=1m/k[(i,j)=1]=d=1n/kμ(d)ndkmdk,整除分块即可。

相同的题:P3455 [POI2007] ZAP-Queries

Code
#include<bits/stdc++.h>
#define ll long long
#define sz(a) ((int) (a).size())
#define vi vector < int >
#define pb emplace_back
using namespace std;
bool vis[50010];
int cnt, p[50010], mu[50010], ms[50010];
void euler(int n) {
  mu[1] = 1;
  for(int i = 2; i <= n; ++i) {
    if(!vis[i])
      mu[i] = -1, p[++cnt] = i;
    for(int j = 1; j <= cnt && i * p[j] <= n; ++j) {
      mu[i * p[j]] = i % p[j] == 0 ? 0 : -mu[i];
      vis[i * p[j]] = 1;
      if(p[j] % i == 0)
        break;
    }
  }
  for(int i = 1; i <= n; ++i)
    ms[i] = ms[i - 1] + mu[i];
}
ll work(int n, int m) {
  if(n > m)
    swap(n, m);
  ll res = 0;
  for(int l = 1, r; l <= n; l = r + 1) {
    r = min(n / (n / l), m / (m / l));
    res += (n / l) * 1ll * (m / l) * (ms[r] - ms[l - 1]);
  }
  return res;
}
void solve() {
  int a, b, c, d, k;
  cin >> a >> b >> c >> d >> k;
  cout << work(b / k, d / k) - work((a - 1) / k, d / k) - work(b / k, (c - 1) / k) + work((a - 1) / k, (c - 1) / k) << "\n";
}
int main() {
  ios :: sync_with_stdio(false);
  cin.tie(0); cout.tie(0);
  euler(50000);
  int T;
  cin >> T;
  while(T--)
    solve();
  return 0;
}

3 P2257 YY的GCD
Problem

给定 n,m,求 1xn1ymgcd(x,y) 为质数的 (x,y) 有多少对。

T=104,n,m107

Sol

不妨令 nm

显然枚举 pprime,变为 pprimei=1n/pj=1m/p[(i,j)=1],反演一下,变为:pprime,pnd=1n/pndpmdpμ(d)。然后发能每次枚举 p 然后数论分块的复杂度是 T×O(nlogn)n,完全不可接受。发现 ndpmdpp,然后 d×p 其实是取满了 [1,n] 的,不妨就枚举 k=dp,然后再枚举 p。于是变为 k=1nnkmkpprime,pkμ(k/p)。这个时候就很简单了,发现原式的 pprime,pkμ(k/p) 其实很好预处理的,不妨把这个值记作 g(k),显然 g(k) 可以通过枚举质数 p 然后更新 p 的倍数的贡献得到,这个的复杂度和埃筛是一样的,为 O(nlnlnn)。然后再处理出 g 的前缀和,数论分块即可做到 O(nlnlnn+Tn)

Code
#include<bits/stdc++.h>
#define ll long long
#define sz(a) ((int) (a).size())
#define vi vector < int >
#define pb emplace_back
using namespace std;
bool vis[10000010];
int cnt, p[10000010], mu[10000010], ms[10000010];
ll g[10000010];
void euler(int n) {
  mu[1] = 1;
  for(int i = 2; i <= n; ++i) {
    if(!vis[i])
      mu[i] = -1, p[++cnt] = i;
    for(int j = 1; j <= cnt && i * p[j] <= n; ++j) {
      mu[i * p[j]] = i % p[j] == 0 ? 0 : -mu[i];
      vis[i * p[j]] = 1;
      if(p[j] % i == 0)
        break;
    }
  }
  for(int i = 1; i <= cnt; ++i)
    for(int j = 1; p[i] * j <= n; ++j)
      g[p[i] * j] += mu[j];
  for(int i = 1; i <= n; ++i)
    ms[i] = ms[i - 1] + mu[i], g[i] += g[i - 1];
}
ll work(int n, int m) {
  if(n > m)
    swap(n, m);
  ll res = 0;
  for(int l = 1, r; l <= n; l = r + 1) {
    r = min(n / (n / l), m / (m / l));
    res += (n / l) * 1ll * (m / l) * (g[r] - g[l - 1]);
  }
  return res;
}
void solve() {
  int n, m;
  cin >> n >> m;
  cout << work(n, m) << "\n";
}
int main() {
  ios :: sync_with_stdio(false);
  cin.tie(0); cout.tie(0);
  euler(10000000);
  int T;
  cin >> T;
  while(T--)
    solve();
  return 0;
}

双倍经验:SP4491

4 P3327 [SDOI2015] 约数个数和
Problem

d(x)x 的约数个数,给定 n,m,求 i=1nj=1md(ij)。多测。

1T,n,m50000

Sol

首先这个 d 是很麻烦的,将它转化一下:d(ij)=a|ib|j[(a,b)=1],证明放最后。然后考虑对式子进行化简(这里默认 nm):

i=1nj=1ma|ib|jϵ((a,b))=a=1nb=1mnambϵ((a,b))=a=1nb=1mnambd|a,d|bμ(d)=d=1nμ(d)(i=1n/dj=1m/dndimdj)=d=1nμ(d)(i=1n/dj=1m/dn/dim/dj)=d=1nμ(d)(i=1n/dn/di)(j=1m/dm/dj)=d=1nμ(d)S(n/d)S(m/d)

这里的 S(n)=i=1nni

然后整除分块即可。

时间复杂度:O((n+T)n)

Code
#include<bits/stdc++.h>
#define ll long long
#define sz(a) ((int) (a).size())
#define vi vector < int >
#define pb emplace_back
using namespace std;
bool vis[50010];
int cnt, p[50010], mu[50010], ms[50010];
void euler(int n) {
  mu[1] = 1;
  for(int i = 2; i <= n; ++i) {
    if(!vis[i])
      p[++cnt] = i, mu[i] = -1;
    for(int j = 1; j <= cnt && i * p[j] <= n; ++j) {
      vis[i * p[j]] = 1;
      mu[i * p[j]] = i % p[j] == 0 ? 0 : -mu[i];
      if(i % p[j] == 0)
        break;
    }
  }
  for(int i = 1; i <= n; ++i)
    ms[i] = ms[i - 1] + mu[i];
}
int s[50010];
ll work(int n) {
  ll res = 0;
  for(int l = 1, r; l <= n; l = r + 1) {
    r = n / (n / l);
    res += (n / l) * (r - l + 1ll);
  }
  return res;
}
void init(int n) {
  euler(n);
  for(int i = 1; i <= n; ++i)
    s[i] = work(i);
}
ll g(int n, int m) {
  return s[n] * 1ll * s[m];
}
int n, m;
void solve() {
  cin >> n >> m;
  if(n > m)
    swap(n, m);
  ll ans = 0;
  for(int l = 1, r; l <= n; l = r + 1) {
    r = min(n / (n / l), m / (m / l));
    ans += (ms[r] - ms[l - 1]) * g(n / l, m / l);
  }
  cout << ans << "\n";
}
int main() {
  ios :: sync_with_stdio(false);
  cin.tie(0); cout.tie(0);
  init(50000);
  int T;
  cin >> T;
  while(T--)
    solve();
  return 0;
}
/*
2
32 43
53 51
*/
posted @   Pengzt  阅读(54)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示