【算法笔记】数学知识

一、数论

①质数

1.质数和合数是针对所有大于1的 "自然数" 来定义的(所有小于等于1的数都不是质数).

2.如果一个整数a>1且只能被1和他自身所整除,则这个数是质数,否则是合数。

3.质数的判定 : 试除法 (时间复杂度是确定的O(sqrt(n)).

(1)"d|n"代表的含义是d能整除n,(这里的"|"代表整除).

(2)一个合数的约数总是成对出现的,如果d|n,那么(n/d)|n,因此我们判断一个数是否为质数的时候,只需要判断较小的那一个数能否整除n就行了,即只需枚举d<=(n/d),即dd<=n,d<=sqrt(n)就行了.

(3)在代码中写循环条件时 我们最好用 i <= n / i这种方式。sqrt(n)函数的执行速度很慢, 每次循环都执行会影响效率。而i * i <= n这种写法当i接近int的最大值时i * i会导致溢出。

cpp 复制代码
bool is_prime(int x)
{
    if(x < 2) return false;
    for(int i = 2; i <= x / i; i++)
        if(x % i == 0)
            return false;
    return true;
}

4.分解质因数 : 试除法 (时间复杂度在O(log n)O(sqrt(n)之间)

(1)一个合数分解而成的质因数最多只包含一个大于sqrt(n)的质因数

(反证法,若n可以被分解成两个大于sqrt(n)的质因数,则这两个质因数相乘的结果大于n,与事实矛盾).

cpp 复制代码
void divide(int x)
{
    for(int i = 2; i <= x / i; i++){
        if(x % i == 0){
            int s = 0;
            while(x % i == 0) x /= i, s++;
            cout << i << ' ' << s << endl;
        }
    }
    if(x > 1) cout << x << ' ' << 1 << endl;
    cout << endl;
}

5.筛质数

(1)朴素筛法(时间复杂度O(nlog n))

做法 :把2~n-1中每个数的倍数全部标记并筛掉,剩下的数就都是质数。

原理 :假定有一个数p未被2~(p-1)中的数标记过,那么说明,不存在2~(p-1)中的任何一个数的倍数是p,也就是说p不是2~(p-1)中的任何数的倍数,也就是说2~(p-1)中不存在p的约数,因此,根据质数的定义可知:p是质数.

cpp 复制代码
void get_primes(int n)
{
    for(int i = 2; i <= n; i++){
        if(!st[i]) primes[cnt++] = i;
        for(int j = i + i; j <= n; j += i)
            st[j] = true;
    }
}

(2)埃式筛法(时间复杂度O(nlog log n))

质数定理1~n中有n/ln n个质数.

整数的唯一分解定理

任何一个大于1的自然数N,如果N不为质数,都可以唯一分解成有限个质数的乘积N=P1a1 ·P2a2·P3a3· ......·Pnan ,这里P1<P2<P3<...<Pn均为质数,其诸指数 ai 是正整数。

所以在之前的朴素筛法的过程中,我们不需要标记所有数的倍数,只需要标记所有质数的倍数即可。

cpp 复制代码
void get_primes(int n)
{
    for(int i = 2; i <= n; i++){
        if(st[i]) continue;
        primes[cnt++] = i;
        for(int j = i + i; j <= n; j += i) st[j] = true;
    }
}

(3)线性筛法(时间复杂度O(n))

线性筛法是对朴素筛法的进一步优化,埃式筛法的缺陷在于,对于同一个合数,可能被筛选多次。为了确保每一个合数只被筛选一次,我们用每个合数的最小质因子来进行筛选

之所以被称为线性,是因为:1~n之内的任何一个合数一定会被筛掉,而且筛的时候只用最小质因子来筛,

每一个数都只有一个最小质因子,所以每个数都只会被筛一次,因此线性筛法是线性的.

具体操作方法为:之前每筛到一个素数,便将这个素数放入到primes[ ]中,在每次循环中,从头开始遍历primes[ ], 筛选出的合数为 primes[j] × i

首先说明:primesj×iprimesj × iprimesj×i的最小质因子应该是 min(primesj,i的最小质因子)min(primesj,i 的最小质因子)min(primesj,i的最小质因子),即二者中的最小值

1.当 i % primes[j] ≠ 0时,说明此时primes[j]小于i的所有质因子,因为 primes[j]是从小到大进行枚举的,如果primes[j]i的质因子之一,那么应该满足i % primes[j] = 0才对。所以此时primes[j]primes[j] × i的最小质因子

2.当 i % primes[j] = 0时说明此时primes[j]i的最小质因子(因为 primes[j]是从小到大进行枚举的),所以此时primes[j]也是primes[j] × i的最小质因子

综上,使用primes[j]来筛选primes[j] × i是可行的,因为两种情况下 primes[j]都是primes[j] × i的最小质因子

那么如何保证每个数都只被筛选一次?即循环应该在何时结束?

循环应该在i % primes[ ] == 0的时候中止,理由如下:

i % primes[j] == 0的时候如果不中止,那么将进入下一次循环,下一次循环要筛掉的数字是 primes[j+1] × i。对于 primes[j+1] × ii的值没有变,和上一步满足 i % primes[j] == 0时的i是一样的,所以当前i的最小质因子仍为primes[j];但是当前为primes[j+1],即比上一次循环的 primes[j]要大,那么此时primes[j+1]i的最小质因子primes[j]相比,较小值为i的最小质因子primes[j],所以primes[j+1] × i的最小质因子应该为primes[j],那么primes[j+1] × i这个数应该由primes[j]来筛选掉,但是当前却是由primes[j+1]筛选掉的,所以出现了重复筛选。

因此,为了保证所有数只被筛选一次,循环需要在 i % primes[ ] == 0的时候中止

若n < 106 ,线性筛和埃氏筛的时间效率差不多; 若 n > 107,线性筛会比埃氏筛快了大概一倍。

cpp 复制代码
void get_primes(int n)
{
    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;
        }
        
    }
}

②约数

1.求约数(试除法 时间复杂度约为O(sqrt(n))

求约数的原理和分解质因数几乎相同,也是遍历到sqrt(n),只需要加一个特判,防止由于n是平方数导致重复放入即可。

cpp 复制代码
vector<int> get_divisors(int x)
{
    vector<int> res;
    for(int i = 1; i <= x / i; i++){
        if(x % i == 0){
            res.push_back(i);
            if(i != x / i) res.push_back(x / i);
        }
    }
    sort(res.begin(), res.end());
    return res;
}

2.从算术基本定理到因数个数与因数之和公式

(1)算术基本定理

算术基本定理(Fundamental Theorem of Arithmetic)指出:

任何大于 111 的正整数 nnn 都可以唯一地(不计顺序地)表示为若干个质数的乘积:

n=p1a1p2a2⋯pkak, n = p_1^{a_1} p_2^{a_2} \cdots p_k^{a_k}, n=p1a1p2a2⋯pkak,

其中 p1,p2,...,pkp_1, p_2, \dots, p_kp1,p2,...,pk 是互不相同的质数,指数 aia_iai 是正整数。

这个定理是整个初等数论的基石,它保证了每个合数的质因数分解是唯一的。

(2)因数的一般形式

设 n=p1a1p2a2⋯pkakn = p_1^{a_1} p_2^{a_2} \cdots p_k^{a_k}n=p1a1p2a2⋯pkak。

如果 ddd 是 nnn 的一个正因数,那么 ddd 的质因数只能从 p1,p2,...,pkp_1, p_2, \dots, p_kp1,p2,...,pk 中选取,且每个质因数 pip_ipi 在 ddd 中的指数 bib_ibi 不能超过它在 nnn 中的指数 aia_iai。于是 ddd 必可写为:

d=p1b1p2b2⋯pkbk,0≤bi≤ai. d = p_1^{b_1} p_2^{b_2} \cdots p_k^{b_k}, \qquad 0 \le b_i \le a_i. d=p1b1p2b2⋯pkbk,0≤bi≤ai.

反过来,任何这样形式的数都是 nnn 的因数.

(3)因数个数公式

对于每个质因数 pip_ipi,指数 bib_ibi 可以取 0,1,2,...,ai0, 1, 2, \dots, a_i0,1,2,...,ai,共有 ai+1a_i + 1ai+1 种可能的取值。由于不同质数的指数选择是相互独立的,因此总的因数个数为:

τ(n)=(a1+1)(a2+1)⋯(ak+1)=∏i=1k(ai+1). \tau(n) = (a_1 + 1)(a_2 + 1) \cdots (a_k + 1) = \prod_{i=1}^{k} (a_i + 1). τ(n)=(a1+1)(a2+1)⋯(ak+1)=i=1∏k(ai+1).

这个函数通常记作 d(n)d(n)d(n) 或 τ(n)\tau(n)τ(n)。

:n=72=23⋅32n = 72 = 2^3 \cdot 3^2n=72=23⋅32,则

τ(72)=(3+1)(2+1)=4×3=12\tau(72) = (3+1)(2+1) = 4 \times 3 = 12τ(72)=(3+1)(2+1)=4×3=12。

(验证:72的正因数有:1,2,3,4,6,8,9,12,18,24,36,72,共12个)

(4)因数之和公式

所有正因数的和 σ(n)\sigma(n)σ(n) 可以通过展开所有可能乘积的和得到:

σ(n)=∑b1=0a1∑b2=0a2⋯∑bk=0ak(p1b1p2b2⋯pkbk). \sigma(n) = \sum_{b_1=0}^{a_1} \sum_{b_2=0}^{a_2} \cdots \sum_{b_k=0}^{a_k} \left( p_1^{b_1} p_2^{b_2} \cdots p_k^{b_k} \right). σ(n)=b1=0∑a1b2=0∑a2⋯bk=0∑ak(p1b1p2b2⋯pkbk).

由于求和号可以分解为各质数幂的独立和之积(因为每个 bib_ibi 独立地跑遍其范围,且乘积求和等于和的乘积):

σ(n)=(∑b1=0a1p1b1)(∑b2=0a2p2b2)⋯(∑bk=0akpkbk). \sigma(n) = \left( \sum_{b_1=0}^{a_1} p_1^{b_1} \right) \left( \sum_{b_2=0}^{a_2} p_2^{b_2} \right) \cdots \left( \sum_{b_k=0}^{a_k} p_k^{b_k} \right). σ(n)=(b1=0∑a1p1b1)(b2=0∑a2p2b2)⋯(bk=0∑akpkbk).

每个括号内是一个等比数列的和:

∑b=0aipib=1+pi+pi2+⋯+piai=piai+1−1pi−1. \sum_{b=0}^{a_i} p_i^{b} = 1 + p_i + p_i^2 + \cdots + p_i^{a_i} = \frac{p_i^{a_i+1} - 1}{p_i - 1}. b=0∑aipib=1+pi+pi2+⋯+piai=pi−1piai+1−1.

因此:

σ(n)=∏i=1kpiai+1−1pi−1. \sigma(n) = \prod_{i=1}^{k} \frac{p_i^{a_i+1} - 1}{p_i - 1}. σ(n)=i=1∏kpi−1piai+1−1.

:n=72=23⋅32n = 72 = 2^3 \cdot 3^2n=72=23⋅32,

σ(72)=24−12−1⋅33−13−1=16−11⋅27−12=15⋅13=195. \sigma(72) = \frac{2^{4} - 1}{2 - 1} \cdot \frac{3^{3} - 1}{3 - 1} = \frac{16 - 1}{1} \cdot \frac{27 - 1}{2} = 15 \cdot 13 = 195. σ(72)=2−124−1⋅3−133−1=116−1⋅227−1=15⋅13=195.

手动验证:72的所有因数之和为

1+2+3+4+6+8+9+12+18+24+36+721+2+3+4+6+8+9+12+18+24+36+721+2+3+4+6+8+9+12+18+24+36+72,计算得195,一致。

(5)公式总结

设 n=p1a1p2a2⋯pkakn = p_1^{a_1} p_2^{a_2} \cdots p_k^{a_k}n=p1a1p2a2⋯pkak(质因数分解),则:

  • 因数个数

    τ(n)=∏i=1k(ai+1) \boxed{\tau(n) = \prod_{i=1}^{k} (a_i + 1)} τ(n)=i=1∏k(ai+1)

  • 因数之和

    σ(n)=∏i=1kpiai+1−1pi−1 \boxed{\sigma(n) = \prod_{i=1}^{k} \frac{p_i^{a_i+1} - 1}{p_i - 1}} σ(n)=i=1∏kpi−1piai+1−1

这两个公式在初等数论中非常常用,例如判断完全数、过剩数等,也是许多数论函数的基础。

因数个数代码

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>

using namespace std;

typedef long long LL;

const int N = 110, mod = 1e9 + 7;

int main()
{
    int n;
    cin >> n;                     // 输入数字的个数
    
    unordered_map<int, int> primes;  // 哈希表,存储质因数及其指数
    
    while(n--){                   // 依次读入每个数
        int x;
        cin >> x;
        
        // 对 x 进行质因数分解
        for(int i = 2; i <= x / i; i++){   // i 枚举可能的质因子,只需检查到 sqrt(x)
            while(x % i == 0){             // 如果 i 能整除 x,说明 i 是质因子
                x /= i;                    // 除掉因子 i
                primes[i]++;               // 指数 +1
            }
        }
        
        // 如果经过上面的分解后 x > 1,说明剩下的 x 是一个大于 sqrt(原x) 的质数
        if(x > 1) primes[x]++;
    }
    
    // 所有数都分解完毕后,根据公式:约数个数 = ∏ (指数 + 1)
    LL res = 1;
    for(auto p : primes) res = res * (p.second + 1) % mod;   // 累乘并取模
    
    cout << res << endl;          // 输出结果
    
    return 0;
}

因数之和代码

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>

using namespace std;

typedef long long LL;

const int N = 110, mod = 1e9 + 7;   // mod 用于取模,防止结果过大

int main()
{
    int n;
    cin >> n;                       // 输入数字的个数
    
    unordered_map<int, int> primes; // 哈希表,存储质因数及其指数(累积所有输入数的质因数)
    
    while(n--){                     // 依次读入每个数
        int x;
        cin >> x;
        
        // 对 x 进行质因数分解
        for(int i = 2; i <= x / i; i++){   // i 枚举可能的质因子,只需检查到 sqrt(x)
            while(x % i == 0){             // 如果 i 能整除 x,说明 i 是质因子
                x /= i;                    // 除掉因子 i
                primes[i]++;               // 对应质因子的指数 +1
            }
        }
        // 如果经过上面的分解后 x > 1,说明剩下的 x 是一个大于 sqrt(原x) 的质数
        if(x > 1) primes[x]++;
    }
    
    // 此时 primes 中存储了所有输入数的乘积的质因数分解结果
    // 根据因数之和公式:σ(n) = ∏ (1 + p + p^2 + ... + p^a)
    // 其中 p 是质因数,a 是对应指数
    
    LL res = 1;                     // 最终结果,初始为1(乘法单位元)
    for(auto p : primes){           // 遍历每个质因数及其指数
        LL a = p.first, b = p.second;  // a 是质数 p,b 是指数
        LL t = 1;                   // 用于计算 1 + p + p^2 + ... + p^b
        while(b--){                 // 循环 b 次,每次乘以 p 并加 1
            t = (t * a + 1) % mod;  // 递推构造几何级数:
                                    // 初始 t = 1
                                    // 第一次迭代:t = 1 * p + 1 = p+1
                                    // 第二次:t = (p+1)*p + 1 = p^2+p+1
                                    // 第三次:t = (p^2+p+1)*p + 1 = p^3+p^2+p+1
                                    // 最终得到 1 + p + p^2 + ... + p^b
        }
        res = res * t % mod;        // 将每个质因子的几何级数乘积累乘到结果中,并取模
    }
    
    cout << res << endl;            // 输出所有输入数乘积的因数之和(取模后)
    
    return 0;
}

3.最大公约数(欧几里得算法)

(1)算法原理

对于正整数 a≥b>0a \ge b > 0a≥b>0,令 r=a mod br = a \bmod br=amodb(即 aaa 除以 bbb 的余数),则

gcd⁡(a,b)=gcd⁡(b,r) \gcd(a, b) = \gcd(b, r) gcd(a,b)=gcd(b,r)

原因 :设 d=gcd⁡(a,b)d = \gcd(a, b)d=gcd(a,b),则 d∣ad \mid ad∣a 且 d∣bd \mid bd∣b。由带余除法 a=qb+ra = qb + ra=qb+r 得 r=a−qbr = a - qbr=a−qb,故 d∣rd \mid rd∣r,从而 ddd 是 bbb 与 rrr 的公因数,所以 gcd⁡(a,b)≤gcd⁡(b,r)\gcd(a, b) \le \gcd(b, r)gcd(a,b)≤gcd(b,r)。反之,任何整除 bbb 与 rrr 的数也整除 qb+r=aqb + r = aqb+r=a,因此 gcd⁡(b,r)≤gcd⁡(a,b)\gcd(b, r) \le \gcd(a, b)gcd(b,r)≤gcd(a,b)。两者相等。

(2)为什么结果一定是最大的

算法反复应用上述等式:

gcd⁡(a,b)=gcd⁡(b,r1)=gcd⁡(r1,r2)=⋯=gcd⁡(rn−1,0)=rn−1 \gcd(a, b) = \gcd(b, r_1) = \gcd(r_1, r_2) = \cdots = \gcd(r_{n-1}, 0) = r_{n-1} gcd(a,b)=gcd(b,r1)=gcd(r1,r2)=⋯=gcd(rn−1,0)=rn−1

每一步的余数严格减小(0≤ri+1<ri0 \le r_{i+1} < r_i0≤ri+1<ri),所以必在有限步内得到余数 000。此时最后一个非零余数 rn−1r_{n-1}rn−1 与 000 的最大公约数就是它自身。由于每一步都保持最大公约数不变,rn−1r_{n-1}rn−1 就是原 gcd⁡(a,b)\gcd(a, b)gcd(a,b)。又因为 rn−1r_{n-1}rn−1 能同时整除 aaa 和 bbb,而任何更大的整数都不可能同时整除它们(否则会大于余数本身),所以它一定是 最大 的公因数。

cpp 复制代码
int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}
相关推荐
xqqxqxxq1 小时前
哈希表(HashMap)技术学习笔记
笔记·学习·散列表
吴可可1231 小时前
AutoCAD 2016与2014二次开发关键差异
算法
AOwhisky1 小时前
MySQL 学习笔记(第四期):SQL 语言之多表查询
linux·运维·网络·数据库·笔记·学习·mysql
xian_wwq2 小时前
【学习笔记】「大模型安全:攻击面演化史」第 07 篇-安全左移
人工智能·笔记·学习
雨白2 小时前
哈希:以时间换空间的算法实战
算法
San813_LDD4 小时前
[数据结构]LeetCode学习
数据结构·算法·图论
x138702859574 小时前
c语言排雷游戏(基础版9*9)
c语言·算法·游戏
nnsix4 小时前
Unity 贴图压缩格式 笔记
笔记·unity·贴图