|
|
##[$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_0,a_1,b_0,b_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≤2∗10^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;
|
|
|
}
|
|
|
```
|
|
|
|