You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

347 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##[$AcWing$ $200$. $Hankson$的趣味题](https://www.acwing.com/problem/content/description/202/)
**[这道题](https://www.cnblogs.com/littlehb/p/15196071.html)** 是姊妹题关系,套路应该是一样的,无脑的写代码:
### 一、题目描述
$Hanks$ 博士是 $BT$$Bio-Tech$,生物技术)领域的知名专家,他的儿子名叫 $Hankson$。
现在,刚刚放学回家的 $Hankson$ 正在思考一个有趣的问题。
今天在课堂上,老师讲解了如何求两个正整数 $c_1$ 和 $c_2$ 的最大公约数和最小公倍数。
现在 $Hankson$ 认为自己已经熟练地掌握了这些知识,他开始思考一个 **求公约数** 和 **求公倍数** 之类问题的 **逆问题**,这个问题是这样的:
已知正整数 $a_0,a_1,b_0,b_1$,设某未知正整数 $x$ 满足:
- $x$ 和 $a_0$ 的最大公约数是 $a_1$
- $x$ 和 $b_0$ 的最小公倍数是 $b_1$
$Hankson$ 的 **逆问题** 就是求出满足条件的正整数 $x$。
但稍加思索之后,他发现这样的 $x$ 并不唯一,甚至可能不存在。
因此他转而开始考虑如何求解满足条件的 $x$ 的个数。
请你帮助他编程求解这个问题。
**输入格式**
输入第一行为一个正整数 $n$,表示有 $n$ 组输入数据。
接下来的 $n$ 行每行一组输入数据,为四个正整数 $a_0a_1b_0b_1$,每两个整数之间用一个空格隔开。
输入数据保证 $a_0$ 能被 $a_1$ 整除,$b_1$ 能被 $b_0$ 整除。
**输出格式**
输出共 $n$ 行。
每组输入数据的输出结果占一行,为一个整数。
对于每组数据:若不存在这样的 $x$,请输出 $0$
若存在这样的 $x$,请输出满足条件的 $x$ 的个数;
**数据范围**
$1≤n≤2000,1≤a_0,a_1,b_0,b_1≤210^9$
**输入样例**
```cpp {.line-numbers}
2
41 1 96 288
95 1 37 1776
```
**输出样例**
```cpp {.line-numbers}
6
2
```
### 二、前置知识
<font color='red'><b>以下性质,都是定义在整数范围内:</b></font>
#### 1、哪个数的约数最多多少个
其实就是求 **[反素数](https://www.cnblogs.com/littlehb/p/16292168.html)** 这个题,共$1600$个。**这个$1600$需要进行记忆**,有很多题在定义上限时需要用到。
#### 2、$1 \sim N$中任何数的不同质因子 <font color='red'><b>个数</b></font> 不会超过$9$个
因为$2×3×5×7×11×13×17×19×23×29>2×10^9$:
一个数,它可以有很大的质数因子,但不同的质数因子个数,按最小的计算都无法超过$9$个,大点的就更不可能超过$9$个了,否则就超过了`INT_MAX`
#### 3、枚举小质数因子需要到的上限值
```cpp {.line-numbers}
cout << sqrt(INT_MAX) << endl;
```
输出:`46341`,所以,一般开数组开到`50000`足够~
#### 4、枚举最大公约数的倍数
枚举$a_1$的倍数
$TLE$ $6/11$个数据
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
// 最大公约数
int gcd(int x, int y) {
return y ? gcd(y, x % y) : x;
}
// 最小公倍数
int lcm(int x, int y) {
return y / gcd(x, y) * x; // 注意顺序防止乘法爆int
}
int main() {
// 输入
int n;
cin >> n;
// 最大2000次噢
while (n--) {
/*
读入四个数字
x 和 a0 的最大公约数是 a1
x 和 b0 的最小公倍数是 b1
*/
int a0, a1, b0, b1;
cin >> a0 >> a1 >> b0 >> b1;
// 每次记数器清0
int cnt = 0;
// 枚举a1的所有倍数
for (int x = a1; x <= b1; x += a1)
if (gcd(x, a0) == a1 && lcm(x, b0) == b1) cnt++;
printf("%d\n", cnt);
}
return 0;
}
```
#### 5、枚举最小公倍数的约数
思考了下,为什么遍历倍数会大面积的$TLE$呢?
下载了测试点$2$的数据:
```cpp {.line-numbers}
2000
21222 2 999993719 1999987438
9034 2 999978442 1999956884
24921 1 999975441 1999950882
...
```
$b_1$很大,接近$2e9$$a_1$很小,比如$1,2$,这样的极限数值,如果用在枚举倍数的时候,就会循环接近$2e9$次,不$tle$才是奇迹!说白了,就是出题人故意造成了些数据 **卡掉了枚举倍数方法**,铁了心 **让我们使用枚举约数的方法**。
同时,我们也认识到,枚举约数可以只运算到$1\sim \sqrt{b_1}$,数据量并不大,性能有保障,以后还是记住套路,<font color='red' size=4><b>尽量枚举最小公倍数的约数</b></font>,这样更靠谱些。
时间复杂度: $O(n\sqrt{b_1})$
由于 $[x,b_0]=b_1$,因此 $x$ 一定是 $b_1$ 的约数。
所以我们可以枚举 $b_1$ 的所有约数,然后依次判断是否满足 $[x,b_0]=b_1$ 以及 $(x,a_0)=a_1$ 即可。
如果直接用试除法求 $b_1$ 的所有约数,那么总计算量是
$n \sqrt{b_1}=2000 \sqrt{2×10^9}≈10^8$,会有一个测试数据超时。
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
// 最大公约数
int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
// 最小公倍数
int lcm(int a, int b) {
return b / gcd(a, b) * a; // 注意顺序防止乘法爆int
}
int main() {
int T;
cin >> T;
while (T--) {
int ans = 0, a0, a1, b0, b1;
cin >> a0 >> a1 >> b0 >> b1;
/*
读入四个数字
x 和 a0 的最大公约数是 a1
x 和 b0 的最小公倍数是 b1
*/
for (int i = 1; i * i <= b1; i++) { // 枚举b1的所有约数
if (b1 % i) continue; // 是因数
if (gcd(i, a0) == a1 && lcm(i, b0) == b1) ans++; // 因数i符合要求
int j = b1 / i; // 另一个因子
if (gcd(j, a0) == a1 && lcm(j, b0) == b1 && i != j) ans++;
}
printf("%d\n", ans);
}
return 0;
}
```
### 五、优化思路
上面的代码,之所以最后两个测试点$TLE$,根本的原因在于$1 \sim \sqrt{n}$的因数试除!
有的不可能的因数,也进行了试除,需要$O(\sqrt{N})$的时间复杂度,这个是慢的原因。
> <font color='red' size=3><b> 注:好神奇,$2023$年$11$月$14$日再次看这道题时,发现上面的代码就可以直接$AC$了,不需要再优化了~</b></font>
下面尝试对这个$O(\sqrt{n})$的算法想办法进行优化:
那么我们应该如何 **快速求出$n$的所有约数**
- 欧拉筛 筛出$1\sim 50000$之间的所有质数(因为$50000^2>2×10^9$
- ② 利用上面的所有质数数组,将$b1$分解质因数,生成$b1$质数因数有哪些,并且,每个质数因数有几个
```cpp {.line-numbers}
for (int i = 0; primes[i] <= t / primes[i]; i++) {
int p = primes[i];
if (t % p == 0) {
int s = 0;
while (t % p == 0) t /= p, s++;
f[fl++] = {p, s}; //记录小质数因子和个数
}
}
```
- ③ 现在得到的只是质数因子的信息,并不是我们想要的约数信息,还需要进行转化,怎么转化呢?使用$dfs$!
举个栗子:$24=2^3*3^1$,约数有$(1,2,3,4,6,8,12,24)$,可以视为
* $1 = 2^0 * 3^0$
* $2= 2^1 * 3^0$
* $3=2^0*3^1$
* $4=2^2*3^1$
* $6=2^1*3^1$
* $8=2^3*3^0$
* $12=2^2*3^1$
* $24=2^3*3^1$
我们用$dfs$方式枚举所有的组合情况,就可以得到$24$的所有约数数组!
```cpp {.line-numbers}
/**
功能:根据分解完成的质数因子数组 获取 所有约数
u走到已经求出的质数因子数组f面前现在是第u个
p: 已经拼接完成的的约数初始值是1,是0的话没法通过质数因子相乘得到结果base=1
*/
void dfs(int u, int p) {
if (u == fl) { // 如果所有质数因子遍历完成 0~fl-1是所有质因子的下标
d[dl++] = p; // 约数又多了一个
return;
}
// 枚举当前质数因子f[u]使用几个,最少是0个最多是f[u].count个
for (int i = 0; i <= f[u].count; i++) {
dfs(u + 1, p);
p *= f[u].prime; // 这两句话用的太漂亮了完美的模拟了要0个要1个要2个...牛B plus!
}
}
```
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 50010;
typedef long long LL;
struct Node {
int prime; // 质数因子
int count; // 个数
} f[10]; // 一维:哪个质数因子,二维:有几个 f:因子
// 根据经验, primes[]={2,3,5,7,11,13,17,19,23}足够分解INT_MAX,共9个就够了
int fl; // 配合数组使用的游标
int d[1610], dl; // 约数数组,约数数组游标 d:约数
// 根据经验INT_MAX中约数个数最多的是1600个,开1610足够。
// 欧拉筛
int primes[N], cnt; // primes[]存储所有素数
bool st[N]; // st[x]存储x是否被筛掉
void get_primes(int n) {
memset(st, 0, sizeof st);
cnt = 0;
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
// 最大公约数,辗转相除法
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
// 最小公倍数
int lcm(int a, int b) {
return b / gcd(a, b) * a; // 注意顺序防止乘法爆int
}
/**
功能:根据分解完成的质数因子数组 获取 所有约数
u走到已经求出的质数因子数组f面前现在是第u个
p: 已经拼接完成的的约数初始值是1,是0的话没法通过质数因子相乘得到结果base=1
*/
void dfs(int u, int p) {
if (u == fl) { // 如果所有质数因子遍历完成 0~fl-1是所有质因子的下标
d[dl++] = p; // 约数又多了一个
return;
}
// 枚举当前质数因子f[u]使用几个,最少是0个最多是f[u].count个
for (int i = 0; i <= f[u].count; i++) {
dfs(u + 1, p);
p *= f[u].prime; // 这两句话用的太漂亮了完美的模拟了要0个要1个要2个...牛B plus!
}
}
int main() {
get_primes(50000); // 求小的质数因子sqrt(INT_MAX)<50000,开50000很保险
int n;
cin >> n;
while (n--) {
/*
读入四个数字
x 和 a0 的最大公约数是 a1
x 和 b0 的最小公倍数是 b1
*/
int a0, a1, b0, b1;
cin >> a0 >> a1 >> b0 >> b1;
fl = 0; // 多组数据,每次注意清零
int t = b1; // 拷贝出来,一直除到没有为止
// 枚举b1的每个质数小因子
for (int i = 0; primes[i] <= t / primes[i]; i++) {
int p = primes[i];
if (t % p == 0) {
int s = 0;
while (t % p == 0) t /= p, s++;
f[fl++] = {p, s}; // 记录小质数因子和个数
}
}
// 如果存在大的质因子那么最多只有一个比如2*7=14中的7,此时t=7
// 也可能b1本身就是一个质数比如131,那么此时t=131
if (t > 1) f[fl++] = {t, 1}; // 记录到质数数组中
// 现在求出的是b1的所有质数因数题目要求的是约数,利用dfs通过质数因子获取所有约数
dl = 0; // 多组测试数据,也清一下零吧!
dfs(0, 1); // 一次dfs将质数因子数组 转换 约数数组,p的默认值是1
int res = 0; // 答案数量
for (int i = 0; i < dl; i++) { // 枚举所有约数
int x = d[i]; // 判断是不是符合题意
if (gcd(a0, x) == a1 && lcm(b0, x) == b1) res++;
}
// 输出结果
printf("%d\n", res);
}
return 0;
}
```