莫比乌斯反演笔记

本文同步发表于我的 洛谷博客

1. 莫比乌斯函数

设正整数 n 的标准分解为 i=1kpici

莫比乌斯函数定义为

μ(n)={0i[1,k],ci>0(1)kotherwise

重要结论 1: d|nμ(d)=[n=1]
。该结论可以用莫比乌斯函数性质证明。

重要结论 2: 莫比乌斯函数是积性函数,即对于 pq,有 μ(pq)=μ(p)μ(q)

2. 狄利克雷卷积

1. 一些常见的数论积性函数:

  • 莫比乌斯函数 μ(n)

  • 欧拉函数 φ(n)1n 中与 n 互质的数的个数。

  • 约数个数 d(n)n 的约数个数。

  • 约数和 σd|nd

  • 元函数 ϵ(n)。即 [n=1]

  • 单位函数 id(n)。即为 n

  • 恒等函数 I(n)。该函数恒为 1。有时也写作 1

其中后三个函数为完全积性函数。

2. 狄利克雷卷积

定义两个数论函数的狄利克雷卷积为 (fg)(n)=d|nf(d)g(nd),读作 fg

其中后面括号里的 n 代表卷积范围。

卷积有下面这些良好的性质:

  • 交换律:fg=gf

  • 结合律:(fg)h=f(gh)

上面的性质可以根据卷积定义证明。这里略过。

3. 常见数论函数的狄利克雷卷积

  • d=11

证明:11=d|n1×1=d(n)

  • ϵ=μ1

上文提到过,d|nμ=[n=1]=ϵ(n)

  • id=φ1

利用欧拉函数性质证明。

(1) φ(1)=1

(2) φ(p)=p1

(3) φ(p2)=p×φ(p)=p2p

(4) 根据性质 (3), 推出 φ(pk)=φ(p)×pk1=pkpk1

(5) i=0cφ(pi)=1+i=1cφ(pi)=1+i=1cpipi1=pc

(6)n=i=1kpici,则利用 (5) 和积性函数的性质即可推出。

  • φ=μid

证明:由于 id=φ1,根据卷积的性质,可以在等式两边同时卷上 μ,得到 idμ=φ1μ。根据 μ1=ϵidμ=φϵ=φ。证毕。

  • σ=id1

证明:σ(n)=d|nd=d|nid(d)×I(nd)=id1(n)

3. 莫比乌斯反演

莫比乌斯反演的形式如下:

如果存在 F(n)=d|nf(d),那么有 f(n)=d|nF(d)μ(nd)

该结论可以利用狄利克雷卷积来证明。

证明:

F(n)=d|nf(d) 写成卷积的形式就是 F=f1(n)。那么对两边同时卷上 μ,可得 Fμ(n)=f1μ(n)。由于 1μ(n)=ϵ,所以原式可化为 Fμ(n)=fϵ(n)=f,即 f(n)=d|nF(d)μ(nd)。证毕。

某些例题:

下面求和的上界省略了下取整符号。

P3455 [POI2007]ZAP-Queries

i=1nj=1m[(i,j)=k]

大力莫反。

i=1nj=1m[(i,j)=k]=i=1nkj=1mk[(i,j)=1]=i=1nkj=1mkμ((i,j))1=i=1nkj=1mkd|(i,j)μ(d)=d=1nμ(d)i=1nkdj=1mkd1=d=1nμ(d)nkdmkd

前面的 μ 可以线筛,后面的可以整除分块套上 μ 前缀和。

void get_mu(int n) {
	mu[1] = 1;
	for (int i = 2; i <= n; i ++ ) {
		if (!is_prime[i]) mu[i] = -1, p[ ++ cnt] = i;
		for (int j = 1; j <= cnt and i * p[j] <= n; j ++ ) {
			is_prime[i * p[j]] = true;
			if (i % p[j] == 0) break;
			mu[i * p[j]] = -mu[i];
		}
	}
	for (int i = 1; i <= n; i ++ )
		s[i] = s[i - 1] + mu[i];
}
int main() {
	get_mu(N - 5);
	scanf("%d", &T);
	while (T -- ) {
		int n, m, k;
		scanf("%d%d%d", &n, &m, &k);
		int ans = 0;
		for (int l = 1, r; l <= min(n, m); l = r + 1) {
			r = min(n / (n / l), m / (m / l));
			ans += (n / (l * k)) * (m / (l * k)) * (s[r] - s[l - 1]);
		}
		printf("%d\n", ans);
	}
	return 0;
}

P2522 [HAOI2011]Problem b

i=abj=cd[(i,j)=k]

上一道题的强化版。将上一道题写成函数 calc(n,m,k),表示求 求 i=1nj=1m[(i,j)=k] 的值。然后利用类似二维前缀和的方式,计算出答案即为 calc(b,d,k)calc(a-1,d,k)calc(c-1,b,k)+calc(a-1,c-1,k)

P1390 公约数的和

i=1nj=i+1n(i,j)

首先考虑计算 i=1nj=1n(i,j)

首先枚举 (i,j),得到 d=1ndi=1nj=1n[(i,j)=d]

继续化简:

d=1ndi=1nj=1n[(i,j)=d]=d=1ndi=1ndj=1nd[(i,j)=1]=d=1ndi=1ndj=1ndμ1(gcd(i,j))=d=1ndi=1ndj=1ndp|(i,j)μ(p)=d=1ndp=1nμ(p)i=1npdj=1npd1=d=1ndp=1ndμ(p)npdnpd令 k = pd,k=1nnknkd|kd×μ(kd)k=1nnknkidμ(k)k=1nnknkφ(k)

φ 可以前缀和,然后整除分块就好了。复杂度 O(n+n),瓶颈在前缀和。

原题要求的就是刚才求的减去 i 之后在除以 2 。这里根据最大公约数的交换律或者对称性易得。

另外,这个题也可以不必卷到 φ。只要卷到 =d=1ndp=1ndμ(p)npdnpd 就可以套两层整除分块了。这样的复杂度是 O(n+n)=O(n),也很优秀。

一天后返回来在看一眼原来推得式子,会发现确实推复杂了。正难则反,直接用 φ1=id 来推。

i=1nj=1n(i,j)=i=1nj=1nφ1((i,j))=i=1nj=1nd|(i,j)φ(d)=d=1nφ(d)i=1ndj=1nd1d=1nφ(d)ndnd

可以发现结果是一样的。

  • 两层整除分块
void get_mu(int n) {
	mu[1] = 1;
	for (int i = 2; i <= n; i ++ ) {
		if (!is_prime[i]) mu[i] = -1, p[ ++ cnt] = i;
		for (int j = 1; j <= cnt and p[j] * i <= n; j ++ ) {
			is_prime[i * p[j]] = true;
			if (i % p[j] == 0) break;
			mu[i * p[j]] = -mu[i];
		}
	}
	for (int i = 1; i <= n; i ++ )
		s[i] = s[i - 1] + mu[i];
}

int prob(int l, int r) {
	return (l + r) * (r - l + 1) >> 1;
}
int calc(int n) {
	int ans = 0;
	for (int l = 1, r; l <= n; l = r + 1) {
		r = n / (n / l);
		ans += (s[r] - s[l - 1]) * (n / l) * (n / l);
	}
	return ans;
}
signed main() {
	scanf("%lld", &n);
	get_mu(n);
	
	int ans = 0;
	for (int l = 1, r; l <= n; l = r + 1) {
		r = n / (n / l);
		ans += prob(l, r) * calc(n / l);
	}
	ans -= n * (n + 1) >> 1;
	printf("%lld\n", ans >> 1);
	return 0;
}
  • 卷到 φ
void get_mu(int n) {
	phi[1] = 1;
	for (int i = 2; i <= n; i ++ ) {
		if (!is_prime[i]) phi[i] = i - 1, p[ ++ cnt] = i;
		for (int j = 1; j <= cnt and i * p[j] <= n; j ++ ) {
			is_prime[i * p[j]] = true;
			if (i % p[j] == 0) {
				phi[i * p[j]] = phi[i] * p[j];
				break;
			}
			phi[i * p[j]] = phi[i] * phi[p[j]];
		}
	}
	for (int i = 1; i <= n; i ++ )
		s[i] = s[i - 1] + phi[i];
}

signed main() {
	scanf("%lld", &n);
	get_mu(n);
	
	int ans = 0;
	for (int l = 1, r; l <= n; l = r + 1) {
		r = n / (n / l);
		ans += (n / l) * (n / l) * (s[r] - s[l - 1]);
	}
	ans -= n * (n + 1) >> 1;
	printf("%lld\n", ans >> 1);
	return 0;
}

第二种做法比第一种做法快约 20ms

P2257 YY的GCD

设质数集为 P,求:

i=1nj=1m[(i,j)P]

又到了愉快的推式子时间:这里设 nm

i=1nj=1m[(i,j)P]=dPi=1nj=1m[(i,j)=d]=dPi=1ndj=1md[(i,j)=1]=dPi=1ndj=1mdμ1(gcd(i,j))=dPi=1ndj=1mdp|(i,j)μ(p)=dPp=1ndμ(p)i=1npdj=1mpd1=dPp=1ndμ(p)npdmpd令 k = pd,k=1nnkmkd|k,dPμ(kd)

然后发现两个下取整可以直接整除分块,瓶颈就在计算 d|k,dPμ(kd) 上。这个玩意可以筛出质数后预处理。

预处理复杂度 O(n+nloglogn),计算复杂度 O(Tn)。总复杂度 O(n+nloglogn+Tn)

看了一下题解,预处理也可以线性筛。不过不想看了,反正能过就行。

void init() {
	mu[1] = 1;
	for (int i = 2; i <= N - 5; i ++ ) {
		if (!is_prime[i]) p[ ++ cnt] = i, mu[i] = -1;
		for (int j = 1; j <= cnt and i * p[j] <= N - 5; j ++ ) {
			is_prime[i * p[j]] = true;
			if (i % p[j] == 0) break;
			mu[i * p[j]] = - mu[i];
		}
	}
	for (int i = 1; i <= cnt; i ++ )
		for (int j = 1; p[i] * j <= N - 5; j ++ )
			f[p[i] * j] += mu[j];
	for (int i = 1; i <= N - 5; i ++ )
		s[i] = s[i - 1] + f[i];
}
void substack() {
	scanf("%lld%lld", &n, &m);
	if (n > m) swap(n, m);
	int ans = 0;
	for (int l = 1, r; l <= n; l = r + 1) {
		r = min(n / (n / l), m / (m / l));
		ans += (n / l) * (m / l) * (s[r] - s[l - 1]);
	}
	printf("%lld\n", ans);
}

P3327 [SDOI2015]约数个数和

d(x)x 的约数个数,给定 n,m,求

i=1nj=1md(ij)

有结论:d(ij)=x|iy|j[(x,y)=1]

于是可以愉快地推柿子了。

i=1nj=1mx|iy|j[(x,y)=1]=x=1ny=1m[(x,y)=1]nxmy=d=1min(n,m)μ(d)x=1ndj=1mdnxdmyd

不妨设 f(n)=i=1nni,可以发现这个玩意就等于 i=1nd(i)。而这个东西可以筛 μ 的时候预处理出来。

于是柿子变成 d=1min(n,m)μ(d)f(nd)f(md)。整出分块即可。总复杂度 O(Tn)。预处理可以用线性筛,但是懒得写了,直接莽了个 O(nlogn) 暴力上去。

void init(int n) {
	mu[1] = 1;
	for (int i = 2; i <= n; i ++ ) {
		if (!is_prime[i]) p[ ++ cnt] = i, mu[i] = -1;
		for (int j = 1; j <= cnt and i * p[j] <= n; j ++ ) {
			is_prime[i * p[j]] = true;
			if (i % p[j] == 0) break;
			mu[i * p[j]] = -mu[i];
		}
	}
	for (int i = 1; i <= n; i ++ )
		mu_s[i] = mu_s[i - 1] + mu[i];
	for (int i = 1; i <= n; i ++ )
		for (int j = i; j <= n; j += i)
			s[j] ++ ;
	for (int i = 1; i <= n; i ++ )
		s[i] += s[i - 1];
}
void substack() {
	scanf("%lld%lld", &n, &m);
	int ans = 0;
	for (int l = 1, r; l <= min(n, m); l = r + 1) {
		r = min(n / (n / l), m / (m / l));
		ans += s[n / l] * s[m / l] * (mu_s[r] - mu_s[l - 1]);
	}
	printf("%lld\n", ans);
}

两年半两天半,终于把莫反学完了。再次小记,以免自己以后再忘掉。

posted @   Link-Cut-Y  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
点击右上角即可分享
微信分享提示