探索约数:试除法,约数之和,最大公约数

引言

约数(Divisor)是数论中的基本概念之一,指能够整除某个数的整数。约数在数学、计算机科学和密码学中有着广泛的应用。本文将详细介绍约数的相关知识,包括试除法求约数、最大公约数算法(如辗转相除法和更相减损术),并阐明这些算法的原理和步骤。


1. 试除法求约数

1.1 算法原理

试除法是一种简单直观的求约数的方法。对于一个数 n n n,如果 d d d n n n 的约数,则 n n n 能被 d d d 整除。通过遍历 1 到 n \sqrt{n} n 的所有整数,可以找到 n n n 的所有约数。

1.2 算法步骤

  1. 初始化:准备一个空列表,用于存储约数。
  2. 遍历整数:从 1 到 n \sqrt{n} n 遍历每个整数 d d d
    • 如果 d d d 能整除 n n n,则将 d d d n d \frac{n}{d} dn 加入约数列表。
  3. 去重与排序:如果 d = n d d = \frac{n}{d} d=dn,则只加入一个;最后对约数列表进行排序。

1.3 示例

  • 对于 n = 12 n = 12 n=12,遍历 1 到 12 ≈ 3.464 \sqrt{12} \approx 3.464 12 3.464
    • d = 1 d = 1 d=1:1 和 12 是约数。
    • d = 2 d = 2 d=2:2 和 6 是约数。
    • d = 3 d = 3 d=3:3 和 4 是约数。
  • 最终约数列表为 { 1 , 2 , 3 , 4 , 6 , 12 } \{1, 2, 3, 4, 6, 12\} {1,2,3,4,6,12}

1.4 练习示例

试除法求约数

ACWing 试除法求约数
题目要求我们将数的约数从小到大排序进行输出,我们只需要利用试除法,把约数全部放入vector中,最后再进行排序即可。

#include 
#include 
#include 
using namespace std;

int main() {
    int numberOfNumbers;
    cin >> numberOfNumbers;  // 输入要处理的数字数量

    for (int currentIndex = 0; currentIndex < numberOfNumbers; currentIndex++) {
        int currentNumber;
        cin >> currentNumber;  // 输入当前需要处理的数字

        vector<int> factors;  // 存储因数

        // 找到所有小于等于 sqrt(currentNumber) 的因数
        for (int factor = 1; factor * factor <= currentNumber; factor++) {
            if (currentNumber % factor == 0) {
                factors.push_back(factor);  // 添加较小的因数
                if (factor != currentNumber / factor) {
                    factors.push_back(currentNumber / factor);  // 添加较大的因数(如果不是平方根)
                }
            }
        }

        // 对因数进行排序
        sort(factors.begin(), factors.end());

        // 输出所有因数
        for (int factor : factors) {
            cout << factor << " ";
        }
        cout << endl;  // 换行,准备输出下一个数字的结果
    }

    return 0;
}

tips:不用vector也可以解决此题,只需正向遍历到 n \sqrt n n 输出factor,再反向遍历输出currentNumber / factor即可。对于 m m m次查询,在时间复杂度上,从 m n m\sqrt n mn -> 2 m n 2m\sqrt n 2mn ,仍然是 O ( n ) O(\sqrt n) O(n )的复杂度。

#include 

using namespace std;

int main() {
    int numberOfNumbers;
    cin >> numberOfNumbers;  // 输入要处理的数字数量

    for (int currentIndex = 0; currentIndex < numberOfNumbers; currentIndex++) {
        int currentNumber;
        cin >> currentNumber;  // 输入当前需要处理的数字
        int factor;
        // 输出所有小于等于 sqrt(currentNumber) 的因数
        for (factor = 1; factor <= currentNumber / factor; factor++) {
            if (currentNumber % factor == 0) {
                cout << factor << " ";  // 输出因数
            }
        }

        // 输出所有大于 sqrt(currentNumber) 的因数
        for (factor = currentNumber / factor; factor > 0; factor--) {
            if (currentNumber % factor == 0) {
                cout << currentNumber / factor << " ";  // 输出对应的较大因数
            }
        }

        cout << endl;  // 换行,准备输出下一个数字的结果
    }

    return 0;
}

约数个数

ACWing 约数个数
给定 n n n个正整数 a i a_i ai,输出这些数的乘积的约数个数,答案对 1 0 9 + 7 10^9+7 109+7 取模。这道题实际是一个排列组合问题。要是想求出每一个数的约数,再进行乘法操作遍历求解乘积的约数个数显然是不可行的,因为开销太大了。对此,我们采取的策略是把每个数都进行分解质因数,这样就可以得到他们乘积的质因子了。而乘积的约数就是这些质因子的组合。就是说,如果我的质因数分解的结果为 p 1 x × p 2 y × p 3 z p_1^x\times p_2^y \times p_3^z p1x×p2y×p3z,那么约数的个数就会有 ( x + 1 ) × ( y + 1 ) × ( z + 1 ) (x+1)\times (y+1) \times (z+1) (x+1)×(y+1)×(z+1)。举个例子:

3
12
18
30

这意味着我们需要计算三个数字 ( 12 , 18 , 30 ) (12, 18, 30) (12,18,30)的乘积的所有约数之和,并对结果取模 1 0 9 + 7 10^9 + 7 109+7
质因数分解
我们将分别对这三个数字进行质因数分解,并记录每个质因数的出现次数。
12 12 12的质因数分解结果是 2 2 × 3 1 2^2 \times 3^1 22×31
18 18 18的质因数分解结果是 2 1 × 3 2 2^1 \times 3^2 21×32
30 30 30的质因数分解结果是 2 1 × 3 1 × 5 1 2^1 \times 3^1 \times 5^1 21×31×51
将上述三个数字的质因数分解结果合并:

  • 质因数2:总出现次数为 2 + 1 + 1 = 4 2 + 1 + 1 = 4 2+1+1=4
  • 质因数3:总出现次数为 1 + 2 + 1 = 4 1 + 2 + 1 = 4 1+2+1=4
  • 质因数5:总出现次数为 1 1 1

根据质因数分解的结果,我们可以计算所有约数的和。而每一个约数,也都将由这些质因数组成,
约数 = 2 0 , 1 , 2 , 3 , 4 × 3 0 , 1 , 2 , 3 , 4 × 5 0 , 1 \text{约数} = 2^{0, 1, 2, 3, 4}\times 3^{0, 1, 2, 3, 4} \times 5^{0, 1} 约数=20,1,2,3,4×30,1,2,3,4×50,1
根据排列祝贺知识, 2 2 2的次数有 5 5 5种选择, 3 3 3也是, 5 5 5的次数有 2 2 2种选择。因此,约数个数为 5 × 5 × 2 = 50 5\times 5 \times 2 = 50 5×5×2=50

#include 
#include 

using namespace std;

const int MAXN = 1e5 + 7;  // 定义数组的最大大小
int factorIndex = 0;        // 记录质因数的数量
map<int, int> factorMap;   // 映射质因数到其在primeFactors中的索引

// 分解质因数函数
void factorize(int number) {
    for (int divisor = 2; divisor * divisor <= number; divisor++) {
        while (number % divisor == 0) {
            if (!factorMap[divisor]) {
                factorMap[divisor] = ++factorIndex;  // 如果该质因数还未记录,则分配一个新索引
            }
            primeFactors[factorMap[divisor]]++;  // 增加质因数的计数
            number /= divisor;  // 将当前数字除以该质因数
        }
    }
    if (number > 1) {  // 如果剩下的数字大于1,它也是一个质因数
        if (!factorMap[number]) {
            factorMap[number] = ++factorIndex;  // 如果该质因数还未记录,则分配一个新索引
        }
        primeFactors[factorMap[number]]++;  // 增加质因数的计数
    }
}

int main() {
    int numberOfNumbers;
    cin >> numberOfNumbers;  // 输入要处理的数字数量

    long long result = 1;
    const long long MOD = 1e9 + 7;  // 模数

    for (int i = 0; i < numberOfNumbers; i++) {
        int currentNumber;
        cin >> currentNumber;  // 输入当前需要处理的数字
        factorize(currentNumber);  // 对当前数字进行质因数分解
    }

    // 计算乘积的所有约数之和,并对结果取模
    for (int i = 1; i <= factorIndex; i++) {
        result = (result * (primeFactors[i] + 1)) % MOD;
    }

    cout << result << endl;  // 输出最终结果
    return 0;
}

约数之和

ACWing 约数之和
给定 n n n个正整数 a i a_i ai,输出这些数的乘积的约数之和,答案对 1 0 9 + 7 10^9+7 109+7 取模。这道题的思路和前面议题是相似的,也需要通过质因数的性质推导约数和的公式。具体来说,乘积的约数就是这些质因子的组合。就是说,如果我的质因数分解的结果为 p 1 x × p 2 y × p 3 z p_1^x\times p_2^y \times p_3^z p1x×p2y×p3z,那么约数的之和就会为 ( p 1 0 + p 1 1 + . . . + p 1 x ) × ( p 2 0 + p 2 1 + . . . + p 2 x ) × ( p 3 0 + p 3 1 + . . . + p 3 x ) (p_1^0 + p_1^1 + ... + p_1^x)\times (p_2^0 + p_2^1 + ... + p_2^x) \times (p_3^0 + p_3^1 + ... + p_3^x) (p10+p11+...+p1x)×(p20+p21+...+p2x)×(p30+p31+...+p3x)。举个例子:

3
12
18
30

这意味着我们需要计算三个数字 ( 12 , 18 , 30 ) (12, 18, 30) (12,18,30)的乘积的所有约数之和,并对结果取模 1 0 9 + 7 10^9 + 7 109+7
质因数分解
我们将分别对这三个数字进行质因数分解,并记录每个质因数的出现次数。
12 12 12的质因数分解结果是 2 2 × 3 1 2^2 \times 3^1 22×31
18 18 18的质因数分解结果是 2 1 × 3 2 2^1 \times 3^2 21×32
30 30 30的质因数分解结果是 2 1 × 3 1 × 5 1 2^1 \times 3^1 \times 5^1 21×31×51
将上述三个数字的质因数分解结果合并:

  • 质因数2:总出现次数为 2 + 1 + 1 = 4 2 + 1 + 1 = 4 2+1+1=4
  • 质因数3:总出现次数为 1 + 2 + 1 = 4 1 + 2 + 1 = 4 1+2+1=4
  • 质因数5:总出现次数为 1 1 1

根据质因数分解的结果,我们可以计算所有约数的和。而每一个约数,也都将由这些质因数组成,
约数 = 2 0 , 1 , 2 , 3 , 4 × 3 0 , 1 , 2 , 3 , 4 × 5 0 , 1 \text{约数} = 2^{0, 1, 2, 3, 4}\times 3^{0, 1, 2, 3, 4} \times 5^{0, 1} 约数=20,1,2,3,4×30,1,2,3,4×50,1
当我们固定 2 2 2的次数为 0 0 0,约数和为 3 0 × 5 0 + 3 1 × 5 0 + 3 2 × 5 0 + 3 3 × 5 0 + 3 4 × 5 0 + 3 0 × 5 1 + 3 1 × 5 1 + 3 2 × 5 1 + 3 3 × 5 1 + 3 4 × 5 1 = 2 0 × ( 3 0 + 3 1 + 3 2 + 3 3 + 3 4 ) × ( 5 0 + 5 1 ) 3^0\times 5^0+ 3^1\times 5^0+ 3^2\times 5^0+ 3^3\times 5^0+3^4\times 5^0+3^0\times 5^1+ 3^1\times 5^1+ 3^2\times 5^1+ 3^3\times 5^1+3^4\times 5^1 = 2^0\times (3^0+ 3^1+ 3^2+ 3^3+3^4) \times (5^0 + 5^1) 30×50+31×50+32×50+33×50+34×50+30×51+31×51+32×51+33×51+34×51=20×(30+31+32+33+34)×(50+51)
同理固定 2 2 2次数为 1 1 1, 约数之和为 2 1 × ( 3 0 + 3 1 + 3 2 + 3 3 + 3 4 ) × ( 5 0 + 5 1 ) 2^1 \times (3^0+ 3^1+ 3^2+ 3^3+3^4) \times (5^0 + 5^1) 21×(30+31+32+33+34)×(50+51)
对此,我们可以归纳出约数之和的形式为
( 2 0 + 2 1 + 2 2 + 2 3 + 2 4 ) × ( 3 0 + 3 1 + 3 2 + 3 3 + 3 4 ) × ( 5 0 + 5 1 ) (2^0+ 2^1+ 2^2+ 2^3+2^4) \times (3^0+ 3^1+ 3^2+ 3^3+3^4) \times (5^0 + 5^1) (20+21+22+23+24)×(30+31+32+33+34)×(50+51)
这样,推广到若干个质因数,我们也做类似的求和即可。

#include 
#include 

using namespace std;

const int MAXN = 1e5 + 7;  // 定义数组的最大大小
int factorIndex = 0;        // 记录质因数的数量
map<int, int> factorMap;   // 映射质因数到其在primeFactors中的索引

// 分解质因数函数
void factorize(int number) {
    for (int divisor = 2; divisor * divisor <= number; divisor++) {
        while (number % divisor == 0) {
            if (!factorMap[divisor]) {
                factorMap[divisor] = ++factorIndex;  // 如果该质因数还未记录,则分配一个新索引
            }
            primeFactors[factorMap[divisor]]++;  // 增加质因数的计数
            number /= divisor;  // 将当前数字除以该质因数
        }
    }
    if (number > 1) {  // 如果剩下的数字大于1,它也是一个质因数
        if (!factorMap[number]) {
            factorMap[number] = ++factorIndex;  // 如果该质因数还未记录,则分配一个新索引
        }
        primeFactors[factorMap[number]]++;  // 增加质因数的计数
    }
}

int main() {
    int numberOfNumbers;
    cin >> numberOfNumbers;  // 输入要处理的数字数量

    long long result = 1;
    const long long MOD = 1e9 + 7;  // 模数

    for (int i = 0; i < numberOfNumbers; i++) {
        int currentNumber;
        cin >> currentNumber;  // 输入当前需要处理的数字
        factorize(currentNumber);  // 对当前数字进行质因数分解
    }

    // 计算乘积的所有约数之和,并对结果取模
    for (int i = 1; i <= factorIndex; i++) {
        result = (result * (primeFactors[i] + 1)) % MOD;
    }

    cout << result << endl;  // 输出最终结果
    return 0;

tips:不用等比数列求和公式的原因,是公式中包含除法,而除法是不能边计算边取模的,涉及到逆元的计算。
tips:不用等比数列求和公式的原因,是公式中包含除法,而除法是不能边计算边取模的,涉及到逆元的计算。
tips:不用等比数列求和公式的原因,是公式中包含除法,而除法是不能边计算边取模的,涉及到逆元的计算。


2. 最大公约数算法

2.1 辗转相除法(欧几里得算法)

2.1.1 算法原理

辗转相除法基于以下原理:两个数的最大公约数(GCD)等于其中较小数与两数相除余数的最大公约数。通过递归或迭代,可以逐步缩小问题规模,直到余数为 0。

2.1.2 算法步骤
  1. 输入:两个整数 a a a b b b(假设 a > b a > b a>b)。
  2. 计算余数:计算 a a a 除以 b b b 的余数 r r r
  3. 递归或迭代
    • 如果 r = 0 r = 0 r=0,则 b b b 是最大公约数。
    • 否则,用 b b b r r r 替换 a a a b b b,重复步骤 2。
  4. 输出:最终的非零余数即为最大公约数。
2.1.3 示例
  • 对于 a = 48 a = 48 a=48 b = 18 b = 18 b=18
    • 48 ÷ 18 = 2 48 \div 18 = 2 48÷18=2 12 12 12
    • 18 ÷ 12 = 1 18 \div 12 = 1 18÷12=1 6 6 6
    • 12 ÷ 6 = 2 12 \div 6 = 2 12÷6=2 0 0 0
  • 最大公约数为 6。
练习示例

ACWing 最大公约数
一个偷懒的做法,就是调用algorithm里的__gcd

#include
#include
using namespace std;
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin>>n;
    while(n--){
        int x, y;
        cin>>x>>y;
        cout<<__gcd(x, y)<<endl;
    }
    return 0;
}

当然自己实现gcd也并不复杂

// 计算两个整数的最大公约数 (GCD) 使用欧几里得算法
int gcd(int x, int y) {
    // 确保 x >= y,这样可以简化后续的逻辑
    if (x < y) {
        swap(x, y);
    }

    // 如果 x 能被 y 整除,则 y 就是最大公约数
    if (x % y == 0) {
        return y;
    }

    // 否则,递归调用 gcd(y, x % y)
    return gcd(y, x % y);
}

2.2 更相减损术

2.2.1 算法原理

更相减损术是一种基于减法求最大公约数的方法。其原理是:两个数的最大公约数等于较大数与较小数的差和较小数的最大公约数。通过不断相减,可以逐步缩小问题规模,直到两数相等。

2.2.2 算法步骤
  1. 输入:两个整数 a a a b b b
  2. 比较大小
    • 如果 a = b a = b a=b,则 a a a 是最大公约数。
    • 如果 a > b a > b a>b,则用 a − b a - b ab 替换 a a a
    • 如果 a < b a < b a<b,则用 b − a b - a ba 替换 b b b
  3. 重复步骤 2:直到 a = b a = b a=b
  4. 输出:最终相等的数即为最大公约数。
2.2.3 示例
  • 对于 a = 48 a = 48 a=48 b = 18 b = 18 b=18
    • 48 − 18 = 30 48 - 18 = 30 4818=30
    • 30 − 18 = 12 30 - 18 = 12 3018=12
    • 18 − 12 = 6 18 - 12 = 6 1812=6
    • 12 − 6 = 6 12 - 6 = 6 126=6
  • 最大公约数为 6。

3. 算法对比与应用场景

3.1 试除法

  • 优点:实现简单,适用于求单个数的约数。
  • 缺点:时间复杂度较高,不适用于大规模数据。

3.2 辗转相除法

  • 优点:效率高,适用于求两个数的最大公约数。
  • 缺点:需要递归或迭代,实现稍复杂。

3.3 更相减损术

  • 优点:实现简单,适用于理解最大公约数的基本原理。
  • 缺点:效率较低,不适用于大规模数据。

4. 总结

  • 试除法:适合求单个数的约数,时间复杂度 O ( n ) O(\sqrt{n}) O(n )
  • 辗转相除法:适合求两个数的最大公约数,时间复杂度 O ( log ⁡ ( min ⁡ ( a , b ) ) ) O(\log(\min(a, b))) O(log(min(a,b)))
  • 更相减损术:适合理解最大公约数的基本原理,但效率较低。

通过本文的介绍,读者可以掌握约数相关算法的原理和步骤。希望本文能够帮助读者深入理解约数及其应用。

如果你对约数或其他数论算法有更多疑问,欢迎在评论区留言讨论!

你可能感兴趣的:(数据结构&算法,算法,最大公约数)