跳转至

数论基础(一)基本概念

章节概述

本章将介绍数论的基本概念,包括整除、约数、质数、合数、最大公约数和最小公倍数等。这些概念是学习数论的基础,在算法竞赛和密码学中都有广泛应用。

学习目标

  • 理解整除和约数的概念
  • 掌握质数和合数的判断方法
  • 学会使用埃拉托斯特尼筛法和线性筛法求质数
  • 掌握欧几里得算法求最大公约数
  • 理解扩展欧几里得算法
  • 学会求最小公倍数

(一)整除和约数

整除的定义

对于整数 a 和 b(b ≠ 0),如果存在整数 q,使得 a = b × q,则称 b 整除 a,记作 b | a。此时: - a 称为 b 的倍数 - b 称为 a 的约数(或因数)

基本性质

  1. 传递性:如果 a | b 且 b | c,则 a | c
  2. 线性性质:如果 a | b 且 a | c,则 a | (b ± c)
  3. 倍数性质:如果 a | b,则对于任意整数 k,有 a | (k × b)

代码实现:判断整除

def is_divisible(a, b):
    """
    判断 b 是否整除 a

    参数:
        a: 被除数
        b: 除数

    返回:
        bool: 如果 b 整除 a 返回 True,否则返回 False

    示例:
        >>> is_divisible(12, 3)
        True
        >>> is_divisible(12, 5)
        False
    """
    if b == 0:
        return False
    return a % b == 0

求约数

方法:遍历 1 到 √n,如果 i 整除 n,则 i 和 n/i 都是约数

def get_divisors(n):
    """
    获取 n 的所有约数

    参数:
        n: 正整数

    返回:
        list: n 的所有约数,按升序排列

    示例:
        >>> get_divisors(12)
        [1, 2, 3, 4, 6, 12]
        >>> get_divisors(16)
        [1, 2, 4, 8, 16]
    """
    divisors = []
    for i in range(1, int(n**0.5) + 1):
        if n % i == 0:
            divisors.append(i)
            if i != n // i:  # 避免重复添加完全平方数的平方根
                divisors.append(n // i)
    return sorted(divisors)

时间复杂度:O(√n)

求约数个数

def count_divisors(n):
    """
    计算 n 的约数个数

    参数:
        n: 正整数

    返回:
        int: 约数的个数

    示例:
        >>> count_divisors(12)
        6
        >>> count_divisors(16)
        5
    """
    count = 0
    for i in range(1, int(n**0.5) + 1):
        if n % i == 0:
            if i == n // i:
                count += 1  # 完全平方数,平方根只计数一次
            else:
                count += 2
    return count

求约数之和

def sum_divisors(n):
    """
    计算 n 的所有约数之和

    参数:
        n: 正整数

    返回:
        int: 所有约数的和

    示例:
        >>> sum_divisors(12)
        28  # 1 + 2 + 3 + 4 + 6 + 12 = 28
    """
    total = 0
    for i in range(1, int(n**0.5) + 1):
        if n % i == 0:
            total += i
            if i != n // i:
                total += n // i
    return total

(二)质数和合数

质数的定义

大于 1 的正整数,如果只有 1 和它本身两个约数,则称为质数(或素数)。否则称为合数。

质数的性质

  1. 2 是唯一的偶质数
  2. 大于 2 的质数都是奇数
  3. 算术基本定理:任何大于 1 的整数都可以唯一地表示为质数的乘积
  4. 质数有无穷多个

质数判断

方法一:试除法

import math

def is_prime(n):
    """
    判断 n 是否为质数(试除法)

    参数:
        n: 正整数

    返回:
        bool: 如果 n 是质数返回 True,否则返回 False

    时间复杂度: O(√n)

    示例:
        >>> is_prime(2)
        True
        >>> is_prime(17)
        True
        >>> is_prime(15)
        False
    """
    if n <= 1:
        return False
    if n <= 3:
        return True
    if n % 2 == 0 or n % 3 == 0:
        return False

    # 检查 6k±1 形式的数
    i = 5
    while i * i <= n:
        if n % i == 0 or n % (i + 2) == 0:
            return False
        i += 6

    return True

优化说明: - 所有大于 3 的质数都可以表示为 6k±1 的形式 - 只需要检查到 √n

埃拉托斯特尼筛法(求质数)

原理:通过标记非质数来筛选出所有质数

def sieve_of_eratosthenes(n):
    """
    埃拉托斯特尼筛法:求小于等于 n 的所有质数

    参数:
        n: 正整数

    返回:
        list: 小于等于 n 的所有质数

    时间复杂度: O(n log log n)
    空间复杂度: O(n)

    示例:
        >>> sieve_of_eratosthenes(30)
        [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
    """
    if n < 2:
        return []

    sieve = [True] * (n + 1)
    sieve[0] = sieve[1] = False

    # 从 2 开始,标记每个质数的倍数
    for i in range(2, int(n**0.5) + 1):
        if sieve[i]:
            # 从 i² 开始标记,因为更小的倍数已经被更小的质数标记过
            for j in range(i*i, n + 1, i):
                sieve[j] = False

    primes = [i for i, is_prime in enumerate(sieve) if is_prime]
    return primes

优化版本:

def optimized_sieve(n):
    """
    优化的埃拉托斯特尼筛法

    优化点:
    1. 只处理奇数(节省一半空间)
    2. 从 i² 开始标记
    """
    if n < 2:
        return []

    sieve = [True] * (n + 1)
    sieve[0] = sieve[1] = False

    # 处理 2
    primes = [2] if n >= 2 else []

    # 只处理奇数
    for i in range(3, n + 1, 2):
        if sieve[i]:
            primes.append(i)
            if i * i <= n:
                for j in range(i*i, n + 1, 2*i):
                    sieve[j] = False

    return primes

线性筛法(欧拉筛)

原理:保证每个合数只被其最小质因子标记一次

def linear_sieve(n):
    """
    线性筛法(欧拉筛):求小于等于 n 的所有质数

    参数:
        n: 正整数

    返回:
        list: 小于等于 n 的所有质数

    时间复杂度: O(n)
    空间复杂度: O(n)

    示例:
        >>> linear_sieve(30)
        [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
    """
    if n < 2:
        return []

    primes = []
    is_composite = [False] * (n + 1)

    for i in range(2, n + 1):
        if not is_composite[i]:
            primes.append(i)

        # 用已找到的质数去标记合数
        for p in primes:
            if i * p > n:
                break
            is_composite[i * p] = True
            if i % p == 0:  # 关键:确保每个合数只被最小质因子标记一次
                break

    return primes

优势: - 时间复杂度为 O(n),比埃拉托斯特尼筛法更快 - 适合处理大范围质数筛选

线段筛法(区间筛)

应用场景:求区间 [L, R] 内的质数,其中 R 很大但 R-L 较小

def segmented_sieve(L, R):
    """
    线段筛法:求区间 [L, R] 内的所有质数

    参数:
        L: 区间下界
        R: 区间上界

    返回:
        list: 区间内的所有质数

    时间复杂度: O((R-L+1) log log R + √R log log √R)

    示例:
        >>> segmented_sieve(100, 150)
        [101, 103, 107, 109, 113, 127, 131, 137, 139, 149]
    """
    # 先筛出 √R 以内的质数
    base_primes = sieve_of_eratosthenes(int(R**0.5))

    # 初始化区间标记
    segment = [True] * (R - L + 1)

    for p in base_primes:
        # 找到第一个 >= L 的 p 的倍数
        start = max(p * p, ((L + p - 1) // p) * p)

        for j in range(start, R + 1, p):
            segment[j - L] = False

    # 收集结果
    primes = []
    for i in range(R - L + 1):
        if segment[i] and (L + i) >= 2:  # 1 不是质数
            primes.append(L + i)

    return primes

(三)最大公约数和最小公倍数

最大公约数(GCD)

定义:能够同时整除两个或多个整数的最大正整数。

符号表示:gcd(a, b) 或 (a, b)

最小公倍数(LCM)

定义:能够被两个或多个整数同时整除的最小正整数。

符号表示:lcm(a, b) 或 [a, b]

GCD 与 LCM 的关系

对于正整数 a 和 b:

gcd(a, b) × lcm(a, b) = a × b

欧几里得算法(求 GCD)

原理:gcd(a, b) = gcd(b, a mod b)

def gcd(a, b):
    """
    使用欧几里得算法求最大公约数

    参数:
        a, b: 整数

    返回:
        int: a 和 b 的最大公约数

    时间复杂度: O(log min(a, b))

    示例:
        >>> gcd(12, 18)
        6
        >>> gcd(15, 25)
        5
        >>> gcd(0, 5)
        5
    """
    while b:
        a, b = b, a % b
    return abs(a)

递归版本:

def gcd_recursive(a, b):
    """欧几里得算法(递归版本)"""
    if b == 0:
        return abs(a)
    return gcd_recursive(b, a % b)

扩展欧几里得算法

功能:不仅求出 gcd(a, b),还求出整数 x, y 使得:

a × x + b × y = gcd(a, b)

def extended_gcd(a, b):
    """
    扩展欧几里得算法

    参数:
        a, b: 整数

    返回:
        tuple: (gcd, x, y),使得 a*x + b*y = gcd(a, b)

    时间复杂度: O(log min(a, b))

    示例:
        >>> gcd_val, x, y = extended_gcd(12, 18)
        >>> print(f"GCD: {gcd_val}, x: {x}, y: {y}")
        GCD: 6, x: -1, y: 1
        >>> print(f"验证: 12*{x} + 18*{y} = {12*x + 18*y}")
        验证: 12*-1 + 18*1 = 6
    """
    if b == 0:
        return abs(a), 1 if a > 0 else -1, 0

    gcd_val, x1, y1 = extended_gcd(b, a % b)
    x = y1
    y = x1 - (a // b) * y1

    return gcd_val, x, y

应用: - 求解线性同余方程 - 求模逆元 - 求解不定方程

求最小公倍数(LCM)

def lcm(a, b):
    """
    求最小公倍数

    参数:
        a, b: 正整数

    返回:
        int: a 和 b 的最小公倍数

    时间复杂度: O(log min(a, b))

    示例:
        >>> lcm(12, 18)
        36
        >>> lcm(15, 25)
        75
    """
    if a == 0 or b == 0:
        return 0
    return abs(a * b) // gcd(a, b)

求多个数的 GCD 和 LCM

def gcd_multiple(numbers):
    """
    求多个数的最大公约数

    参数:
        numbers: 整数列表

    返回:
        int: 所有数的最大公约数

    示例:
        >>> gcd_multiple([12, 18, 24])
        6
    """
    if not numbers:
        return 0

    result = numbers[0]
    for num in numbers[1:]:
        result = gcd(result, num)

    return result

def lcm_multiple(numbers):
    """
    求多个数的最小公倍数

    参数:
        numbers: 正整数列表

    返回:
        int: 所有数的最小公倍数

    示例:
        >>> lcm_multiple([4, 6, 8])
        24
    """
    if not numbers:
        return 0

    result = numbers[0]
    for num in numbers[1:]:
        result = lcm(result, num)

    return result

实战练习

练习1:完美数

问题:判断一个数是否为完美数(等于其所有真约数之和)

def is_perfect_number(n):
    """
    判断 n 是否为完美数

    完美数:等于其所有真约数(不包括自身)之和的数
    例如:6 = 1 + 2 + 3

    示例:
        >>> is_perfect_number(6)
        True
        >>> is_perfect_number(28)
        True
        >>> is_perfect_number(12)
        False
    """
    if n <= 1:
        return False

    return sum_divisors(n) - n == n

练习2:亲和数

问题:判断两个数是否为亲和数(各自的真约数之和等于对方)

def are_amicable_numbers(a, b):
    """
    判断两个数是否为亲和数

    亲和数:a 的真约数之和等于 b,b 的真约数之和等于 a
    例如:220 和 284 是亲和数

    示例:
        >>> are_amicable_numbers(220, 284)
        True
        >>> are_amicable_numbers(1184, 1210)
        True
    """
    if a == b:
        return False

    sum_a = sum_divisors(a) - a
    sum_b = sum_divisors(b) - b

    return sum_a == b and sum_b == a

练习3:第 n 个质数

问题:求第 n 个质数

def nth_prime(n):
    """
    求第 n 个质数

    参数:
        n: 正整数

    返回:
        int: 第 n 个质数

    示例:
        >>> nth_prime(1)
        2
        >>> nth_prime(5)
        11
        >>> nth_prime(10)
        29
    """
    if n <= 0:
        return None

    count = 0
    candidate = 2

    while count < n:
        if is_prime(candidate):
            count += 1
            if count == n:
                return candidate
        candidate += 1

    return None

章节小结

本章介绍了数论的基本概念,包括:

  1. 整除和约数:理解整除的概念,掌握求约数的方法
  2. 质数和合数:学会判断质数,掌握埃拉托斯特尼筛法和线性筛法
  3. 最大公约数和最小公倍数:掌握欧几里得算法和扩展欧几里得算法

这些基础概念在后续章节中会频繁使用,请务必熟练掌握。

下一步

继续学习第二章:模运算,了解模运算的基本概念和模逆元。


练习答案提示: - 完美数:6, 28, 496, 8128, ... - 亲和数:(220, 284), (1184, 1210), (2620, 2924), ...