b440. 互質對

contents

  1. 1. Problem
  2. 2. Sample Output
  3. 3. Solution
    1. 3.1. 前言
    2. 3.2. 參考資源
    3. 3.3. 基礎定義
      1. 3.3.1. 積性函數
      2. 3.3.2. 歐拉函數
      3. 3.3.3. 莫比烏斯
    4. 3.4. 應用此題
    5. 3.5. 一般解
    6. 3.6. 加速運算解

Problem

給定 $n$$m$,請你統計有序對 $(a,b)$ 的個數,其中 $1 \le a \le n, 1 \le b \le m$$a$$b$ 互質。

$a$$b$ 互質的定義是:$a$$b$ 的最大公約數等於 $1$

> count coprime pair

## Sample Input ##
1
2
3
2
3 4
10000000 10000000

Sample Output

1
2
9
60792712854483

Solution

前言

柳柳州出數論基礎-「莫比烏斯反演 (Möbius inversion formula)」

之前放棄看完的內容又要撿回來看,百般痛苦,之前沒看懂啊。但能知道的是莫比烏斯反演類似排容原理,就像錯排依樣。先不管莫比烏斯,看到「線性時間求出所有乘法反元素」真的可以嗎?那也是件很有趣的事情。

參考資源

  • 莫比烏斯參考《線性篩法與積性函數》-賈志鵬 link
  • 分塊優化參考《POI XIV Stage.1 Queries Zap 解题报告》-Kwc Oliver link

基礎定義

了解莫比烏斯反演之前,要先介紹積性函數

積性函數

  • 何謂積性函數 (Multiplicative function)?
    對於定義域 $\mathbb{N}^+$ 的函數 $f$ 而言,任兩個互質的 $gcd(a, b) = 1$ 正整數 $a, b$ 滿足 $f(ab) = f(a)f(b)$
  • 何謂完全積性函數?
    $f$ 是一個積性函數,同時滿足 $f(p^n) = f(p)^n$
  • $f(n), g(n)$ 是積性函數,則 $h(n) = f(n) g(n)$ 也是積性函數。
  • $f(n)$ 為積性函數,則函數 $g(n) = \sum_{d|n} f(d)$ 也是積性函數。

歐拉函數

回顧歐拉函數 (Euler’s totient function) $\phi$

  • 定義 $\phi(n)$$1 \cdots n$ 中與 $n$ 互質的個數。
  • $\phi(n)$ 是一個積性函數,但不是完全積性函數。根據中國餘數定理 或者 基本算術可以證明之。

  • 歐拉定理 $a^{\phi(n)} \equiv 1 \mod n, \text{ when } gcd(a, n) = 1$

  • 特性 1:$\sum_{d|n} \phi(d) = n$。當作把互質個數 $\phi(n)$ 相加不互質個數 $s$,由於 $d$$n$ 的因數,則與 $d$ 互質 $x$ 個數 $\phi(d)$,把那些 $x' = x \times d/n$ 就會補上那些不互質的個數 $s = |set(x')|$

  • 特性 2:$1 \cdots n$$n$ 互質的數和為 $n\phi(n)/2$。原因很簡單,若 $gcd(x, n) = 1$,則會滿足 $gcd(n-x, n) = 1$,看起來就是一個對稱總和。

莫比烏斯

根據積性函數的性質,我們得到莫比烏斯反演的基礎:

莫比烏斯反演公式 $f(n)$

$f(n) = \sum_{d|n} \mu(d) g(\frac{n}{d})$

莫比烏斯函數 $\mu(n)$

$$\mu(n) = \left\{\begin{matrix} 1 && n = 1\\ (-1)^k && n = p_1 p_2 \cdots p_k \\ 0 && \text{otherwise} \end{matrix}\right.$$
  • 特性 1:$\sum_{d|n} \mu(d) = [n = 1]$

在此簡單說一次,莫比烏斯反演公式就像排容原理,而莫比烏斯函數 $\mu(n)$ 就像一加一減,之所以在 $n = p_1 p_2 \cdots p_k$$\mu(n) = (-1)^k$,也就是說當 $n$ 只全由質數相乘 (不允許冪次方大於 1,否則為 0),相當於一般認知,奇數次要扣,偶數次要補回來的排容口訣。而特定自然不必多說,其中數學表達 $[n = 1]$ 相當於程式中的 n == 1 ? 1 : 0

簡單展示歐拉函數 $\phi(n)$

  • $f(n) = \sum_{d|n} \phi(d) = n$,由歐拉函數特性 2 得知。
    *$\phi(n) = \sum_{d|n} \mu(d) f(\frac{n}{d}) = \sum_{d|n} \frac{\mu(d) n}{d}$,套用莫比烏斯公式後整理。

關於公式$\phi(n) = \sum_{d|n} \frac{\mu(d) n}{d}$ 可以這麼理解。

  • 求出 $gcd(n, k) = 1$ 的個數,其餘要捨棄掉。
  • 計算 $gcd(n, k) = d$ 的個數,利用排容原理即莫比烏斯函數,顯而易見排容方案只當 $d$$n$ 個質因數組合而成。

應用此題

利用莫比烏斯函數 特性 1:$\sum_{d|n} \mu(d) = [n = 1]$

$$\begin{align*} & \sum_{a = 1}^{N} \sum_{b = 1}^{M} [gcd(a, b) = 1] \\ & = \sum_{a = 1}^{N} \sum_{b = 1}^{M} \sum_{d|gcd(a, b)} \mu(d) \\ & = \sum \mu(d) \left ( \sum_{1 \le a \le N \text{ and } d | a} \left ( \sum_{1 \le b \le M \text{ and } d | b} 1 \right ) \right )\\ & = \sum \mu(d) \left \lfloor \frac{n}{d} \right \rfloor \left \lfloor \frac{m}{d} \right \rfloor \end{align*}$$

第二行就是抽換莫比烏斯,第三行則是交換順序,第四行則是可以快速找到 $d|a$ 的總數為 $\lfloor n/d \rfloor$ 所導致。

若直接窮舉 $d$ 時間複雜度為 $O(min(n, m))$,可以利用塊狀的概念優化到 $O(\sqrt{n} + \sqrt{m})$。因為 $\lfloor n/d \rfloor$ 的值只有 $2\lfloor \sqrt{n} \rfloor$ 種可能,同理 $\lfloor m/d \rfloor$ 也是,那麼對於同一個 $d$ 的計數會是一個連續區間不斷地移動。

以下是一個簡單的 $\lfloor n/d \rfloor$ 示意圖,用 $\lfloor n/d \rfloor$ 不同當作劃分。

1
2
3
4
5
6
7
8
9
10
11
X X X X X X X XX X X
X X X X X X X XX X X
+----------------------------------------------------+
| | | | | | | | | n
+----------------------------------------------------+
012345678901234567890123456789012345678901234567890123
+---------------------------------------------------------------------------+
| | | | | | | | | | | | m
+---------------------------------------------------------------------------+
X X X X X X X XX X X
X X X X X X X XX X X

程式只要處理打 X 的位置即可,時間複雜度 $O(\sqrt{n} + \sqrt{m})$,可以參照一般解。代碼短,但除法次數多。

加速一般解由 liouzhou_101 提供。開一次根號 sqrt(),省下 $O(\sqrt{n})$ 次的除法,賺了 400 ms,代碼長了 800 B。加速 25% 的效能,可謂除法的可怕,根據研究差異後才發現到這一點。

一般解

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
#include <bits/stdc++.h>
using namespace std;
#define GET(x) (mark[(x)>>5]>>((x)&31)&1)
#define SET(x) (mark[(x)>>5] |= 1<<((x)&31))
const int MAXN = 10000005;
const int MAXL = (MAXN>>5)+1;
int mark[MAXL];
int P[700000], Pt = 0;
short mu[MAXN], sum[MAXN];
void sieve_mobius() {
register int i, j, k;
SET(1), mu[1] = 1;
int n = 10000000;
for (i = 2; i <= n; i++) {
if (!GET(i))
P[Pt++] = i, mu[i] = -1;
for (j = 0; j < Pt && (k = i*P[j]) <= n; j++) {
SET(k);
if (i%P[j] == 0) {
mu[k] = 0;
break;
}
mu[k] = -mu[i];
}
}
}
long long coprime_pair(int n, int m) {
long long ret = 0;
if (n > m) swap(n, m);
for (int d = 1, r; d <= n; d = r+1) {
r = min(n / (n/d), m / (m/d));
ret += (long long)(sum[r] - sum[d-1]) * (n/d) * (m/d);
}
return ret;
}
int main() {
sieve_mobius();
for (int i = 1; i < MAXN; i++)
sum[i] = sum[i-1] + mu[i];
int testcase, N, M;
scanf("%d", &testcase);
while (testcase--) {
scanf("%d %d", &N, &M);
printf("%lld\n", coprime_pair(N, M));
}
return 0;
}

加速運算解

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
#include <bits/stdc++.h>
using namespace std;
#define GET(x) (mark[(x)>>5]>>((x)&31)&1)
#define SET(x) (mark[(x)>>5] |= 1<<((x)&31))
const int MAXN = 10000005;
const int SQRN = 3200 * 2;
const int MAXL = (MAXN>>5)+1;
int mark[MAXL];
int P[700000], Pt = 0;
short mu[MAXN], sum[MAXN];
void sieve_mobius() {
register int i, j, k;
SET(1), mu[1] = 1;
int n = 10000000;
for (i = 2; i <= n; i++) {
if (!GET(i))
P[Pt++] = i, mu[i] = -1;
for (j = 0; j < Pt && (k = i*P[j]) <= n; j++) {
SET(k);
if (i%P[j] == 0) {
mu[k] = 0;
break;
}
mu[k] = -mu[i];
}
}
}
long long coprime_pair(int n, int m) {
static int An[SQRN][2], Bn[SQRN][2];
if (n > m) swap(n, m);
long long ret = 0;
int aidx = 0, bidx = 0, sq;
sq = sqrt(n);
for (int i = 1; i <= sq; i++, aidx++) An[aidx][1] = n/(An[aidx][0] = n / i);
if (sq * sq == n) aidx--;
for (int i = sq; i >= 1; i--, aidx++) An[aidx][1] = n/(An[aidx][0] = i);
sq = sqrt(m);
for (int i = 1; i <= sq; i++, bidx++) Bn[bidx][1] = m/(Bn[bidx][0] = m / i);
if (sq * sq == m) bidx--;
for (int i = sq; i >= 1; i--, bidx++) Bn[bidx][1] = m/(Bn[bidx][0] = i);
for (int l = 1, r, *a = &An[0][1], *b = &Bn[0][1], *A = &An[0][0], *B = &Bn[0][0]; l <= n; l = r+1) {
if (*a < *b)
r = *a, ret += (long long) (sum[r] - sum[l-1]) * *A * *B, A+=2, a+=2;
else if (*a > *b)
r = *b, ret += (long long) (sum[r] - sum[l-1]) * *A * *B, B+=2, b+=2;
else
r = *a, ret += (long long) (sum[r] - sum[l-1]) * *A * *B, A+=2, B+=2, a+=2, b+=2;
}
return ret;
}
int main() {
sieve_mobius();
for (int i = 1; i < MAXN; i++)
sum[i] = sum[i-1] + mu[i];
int testcase, N, M;
scanf("%d", &testcase);
while (testcase--) {
scanf("%d %d", &N, &M);
printf("%lld\n", coprime_pair(N, M));
}
return 0;
}