Files
python/TangDou/AcWing_TiGao/T5/RongChi/215.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

173 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

##[$AcWing$ $215$. 破译密码](https://www.acwing.com/problem/content/description/217/)
### 一、题目描述
达达正在破解一段密码,他需要回答很多类似的问题:
对于给定的整数 $a,b$ 和 $d$,有多少正整数对 $x,y$,满足 $x≤a,y≤b$,并且 $gcd(x,y)=d$。作为达达的同学,达达希望得到你的帮助。
**输入格式**
第一行包含一个正整数 $n$,表示一共有 $n$ 组询问。
接下来 $n$ 行,每行表示一个询问,每行三个正整数,分别为 $a,b,d$。
**输出格式**
对于每组询问,输出一个正整数,表示满足条件的整数对数。
**数据范围**
$1≤n≤50000,1≤d≤a,b≤50000$
**输入样例:**
```cpp {.line-numbers}
2
4 5 2
6 4 3
```
**输出样例:**
```cpp {.line-numbers}
3
2
```
提示:$gcd(x,y)$ 返回 $x,y$ 的最大公约数。
### 二、题目分析
#### 前导知识
**[$1$、容斥原理 $AcWing$ $890$. 能被整除的数 ](https://www.cnblogs.com/littlehb/p/15389237.html)**
> **小结**:
> ① 二进制对质数序列的组合式枚举
> ② 奇数加,偶数减
**[$2$、数论分块 $AcWing$ $199$. 余数之和](https://www.cnblogs.com/littlehb/p/16389420.html)**
> **小结**:
> 给出$n,k$,求 $\displaystyle \sum_{i=1}^n k \ \% \ i$
**核心代码**
```cpp {.line-numbers}
ans = n * k; // 看题解的推导公式
for (l = 1; l <= n; l = r + 1) { // 枚举左端点,每次跳着走,下次的位置就是本次r的位置+1
if (k / l == 0) break; // 1、当k/l=0的时候,显然这段以及后面(有单调性)已经没有贡献了,可以 break。
r = min(k / (k / l), n); // 2、注意右端点和n取个min,因为>n没有贡献了。
ans -= (k / l) * (l + r) * (r - l + 1) / 2; // 等差数列求和:左到右边界内,是公差为1的等差数列,首项+末项 乘以 项数 除以2
}
```
**[$3$、莫比乌斯函数](https://www.cnblogs.com/littlehb/p/16392135.html)**
#### 解题思路
我们分析一下,题目很明了,要求的就是$gcd(x,y)=d$的个数,转化一下:
$$\large x'=x/d,y'=y/d$$
$$\large |gcd(x,y)=d| \Leftrightarrow |gcd(x',y')=1|$$
><font color='red'><b>注</b>:上面的"| |",表示的是符合这样条件的$(x,y)$的数对个数,这两个式子的数对$(x,y)$和$(x',y')$ 是一一对应的。</font>
> **证明**:
首先,$x , y$ 的最大公约数是$d$,那么 **除去最大公约数**,得到的两个数就 **互质**。对于每个$gcd(x,y)=d$都如此操作,那么可以证明,这两个的 **数量** 是一一对应的。
问题转化为:$x≤a/d$ , $y≤b/d$ ,$gcd(x,y)=1$的个数,我们考虑:
<h4>补集思想:互质数 = 总数 - 不互质数</h4>
总数很容易,$a'=a/d,b'=b/d$ ,两个数的所有组合形式数量就是$a'∗b'$,下面分析下我们要减去不互质的数:
$$\large a'b'-⌊\frac{a'}{2}⌋⌊\frac{b'}{2}⌋-⌊\frac{a'}{3}⌋⌊\frac{b'}{3}⌋-...\\
\ \ \ \ \ \ \ +⌊\frac{a'}{6}⌋⌊\frac{b'}{2}⌋+.... \\
\ \ \ \ \ \ \ -.....
$$
这样看来,本题是一个简单的容斥原理!
但是由于容斥原理是$O(N)$的,询问的次数$50000$次,会超时,才会想到需要优化。
**优化办法**
① 莫比乌斯函数
$$\displaystyle \large a' \cdot b'+\sum_{i=1}^{min(a,b)}⌊\frac{a'}{i}⌋⌊\frac{b'}{i}⌋ \cdot {mobius[i]}$$
② 数据分块
参考余数之和。
$$\large \sum_{i=2}^{min(a',b')}⌊\frac{a'}{i}⌋*⌊\frac{b'}{i}⌋*mobius[i]$$
> **证明**:
> 我们要减去的是什么呢?是不互质的数,不互质数是什么呢?是最大公因数不为$1$的数,不为$1$,那可以为几呢?**不是$1$就行**(~~废话~~),**枚举最大公因数不为$1$的个数即可**,枚举每一个可能的最大公因数,并且计算个数,其中 $2 <= i <= min(a',b')$ 。
>
当最大公因数是只有一个质因子的时候,我们可以列出:
$$\large ⌊\frac{a'}{2}⌋* ⌊\frac{b'}{2}⌋+⌊\frac{a'}{3}⌋*⌊\frac{b'}{3}⌋+.....$$
最大公因数是$2$的两对数,实际上就等于$2$的倍数个数相乘,这两个个数是等同的,同理$3$也是,但是我们要考虑重复的情况,如$6$,我们$2,3$,都枚举了一次,那么会重复,根据容斥原理,我们要减去,最后我们发现,其实符号就是$Mobius$函数(这个证明是显然的)。以此类推,枚举到$a′$和$b′$中较小的就可以了,因为大于较小的,则式子为$0$,我们就能推出上面的式子了。
答案呼之欲出:
$$\large a'*b'+\sum_{i=2}^{min(a',b')}⌊\frac{a'}{i}⌋*⌊\frac{b'}{i}⌋*mobius[i]$$
我们把这玩意合体一下
$$\large \sum_{i=1}^{min(a',b')}⌊\frac{a'}{i}⌋*⌊\frac{b'}{i}⌋*mobius[i]$$
- $\large ⌊\frac{a}{i}⌋×⌊\frac{b}{i}⌋$ 对应的序列有很多值都是相同的,计算一次就行(**[数论分块](https://www.cnblogs.com/littlehb/p/16389420.html)** 优化一下),假设值记为$t$,对应的区间假设为$[l, r]$,然后让$t$乘以$μ(l)+μ(l+1)+…+μ(r)$ 即可求出该段中的和,$μ(l)+μ(l+1)+…+μ(r)$,可以使用 **莫比乌斯前缀和** 求解。
### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int N = 50010;
// 线性筛法求莫比乌斯函数(枚举约数)
int mu[N], sum[N]; // 莫比乌斯函数的前缀和
int primes[N], cnt;
bool st[N];
void get_mobius(int n) {
mu[1] = 1;
for (int i = 2; i <= n; i++) {
if (!st[i]) {
primes[cnt++] = i;
mu[i] = -1;
}
for (int j = 0; primes[j] * i <= n; j++) {
int t = primes[j] * i;
st[t] = true;
if (i % primes[j] == 0) {
mu[t] = 0;
break;
}
mu[t] = -mu[i];
}
}
// 维护莫比乌斯函数前缀和
for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + mu[i];
}
signed main() {
// 筛法求莫比乌斯函数
get_mobius(N - 1);
int T;
cin >> T;
while (T--) {
int a, b, d;
cin >> a >> b >> d;
// 套路啊,满满的套路,直接先用最大公约数a/gcd(a,b)=a',b/gcd(a,b)=b',映射到a',b'
a /= d, b /= d;
// n为 min(a', b')
int n = min(a, b);
int res = 0;
// l r, 是每一段的左右边界
// 每次只能取较小的那个上界作为这一段的右端点r
// 然后下次迭代时下一段的左端点就是r + 1
for (int l = 1, r; l <= n; l = r + 1) { // 分块大法
r = min(n, min(a / (a / l), b / (b / l)));
res += (sum[r] - sum[l - 1]) * (a / l) * (b / l);
}
cout << res << endl;
}
}
```