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.

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的趣味题

这道题 是姊妹题关系,套路应该是一样的,无脑的写代码:

一、题目描述

Hanks 博士是 BTBio-Tech,生物技术)领域的知名专家,他的儿子名叫 Hankson

现在,刚刚放学回家的 Hankson 正在思考一个有趣的问题。

今天在课堂上,老师讲解了如何求两个正整数 c_1c_2 的最大公约数和最小公倍数。

现在 Hankson 认为自己已经熟练地掌握了这些知识,他开始思考一个 求公约数求公倍数 之类问题的 逆问题,这个问题是这样的:

已知正整数 a_0,a_1,b_0,b_1,设某未知正整数 x 满足:

  • xa_0 的最大公约数是 a_1
  • xb_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

输入样例

2
41 1 96 288
95 1 37 1776

输出样例

6
2

二、前置知识

以下性质,都是定义在整数范围内:

1、哪个数的约数最多多少个

其实就是求 反素数 这个题,共1600个。这个1600需要进行记忆,有很多题在定义上限时需要用到。

2、1 \sim N中任何数的不同质因子 个数 不会超过9

因为2×3×5×7×11×13×17×19×23×29>2×10^9: 一个数,它可以有很大的质数因子,但不同的质数因子个数,按最小的计算都无法超过9个,大点的就更不可能超过9个了,否则就超过了INT_MAX

3、枚举小质数因子需要到的上限值

  cout << sqrt(INT_MAX) << endl;

输出:46341,所以,一般开数组开到50000足够~

4、枚举最大公约数的倍数

枚举a_1的倍数 TLE 6/11个数据

#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的数据:

2000
21222 2 999993719 1999987438
9034 2 999978442 1999956884
24921 1 999975441 1999950882
...

b_1很大,接近2e9a_1很小,比如1,2,这样的极限数值,如果用在枚举倍数的时候,就会循环接近2e9次,不tle才是奇迹!说白了,就是出题人故意造成了些数据 卡掉了枚举倍数方法,铁了心 让我们使用枚举约数的方法

同时,我们也认识到,枚举约数可以只运算到1\sim \sqrt{b_1},数据量并不大,性能有保障,以后还是记住套路,尽量枚举最小公倍数的约数,这样更靠谱些。

时间复杂度: 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,会有一个测试数据超时。

#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})的时间复杂度,这个是慢的原因。

注:好神奇,20231114日再次看这道题时,发现上面的代码就可以直接AC了,不需要再优化了~

下面尝试对这个O(\sqrt{n})的算法想办法进行优化:

那么我们应该如何 快速求出n的所有约数

  • ① 欧拉筛 筛出1\sim 50000之间的所有质数(因为50000^2>2×10^9

  • ② 利用上面的所有质数数组,将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}; //记录小质数因子和个数
        }
    }
    
  • ③ 现在得到的只是质数因子的信息,并不是我们想要的约数信息,还需要进行转化,怎么转化呢?使用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的所有约数数组!
/**
功能:根据分解完成的质数因子数组 获取 所有约数
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

#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;
}