跳过正文
  1. posts/

基础数论 - 模板 & 分析

·1766 字·4 分钟·
算法与数据结构 数论 模板
陈驰水
作者
陈驰水
感谢您看到这里,祝您生活愉快
目录
此博客为数论基础板子,以及一些注意事项和例题。

数论内容知识点比较零碎且容易遗忘,因此便对其进行总结。

内容都为数论基础知识点,板子主要用 C++ 表示。

快速幂
#

标准的 a ^ b mod p 形式的快速幂:

long long fast_pow(long long i, long long n, long long mod) {
    long long res = 1;
    i = i % mod;
    while (n) {
        if (n & 1) 
            res = (res * i) % mod;
        i = (i * i) % mod;
        n >>= 1;
    }
    return res;
}

时间复杂度为:

$$ O(\log b) $$

如果要考虑非正数整数则:

double newX = 0;
if (b == 0)
	return 1;
if (b < 0) {
	b = -b;
	newX = 1 / x;  
}

C++ 的 pow() 库函数处理的是浮点数,因此在运行数值较大是会出现精度错误。在比赛或面试中,遇到指数大于十的幂运算时,就建议手搓快速幂。

python 的 pow() 不会有这个问题,可以放心使用。

标准的 a ^ b 形式的快速幂:

long long fast_pow(long long a, long long b) {
    long long res = 1;
    long long base = a;
    while (b) {
        if (b & 1) 
            res = (res * base);
        base = (base * base);
        b >>= 1;
    }
    return res;
}

最大公约数
#

一般而言,用库函数就可以了。

注意 gcd() 在 c++17 才引用,C++11 用 __gcd()。

辗转相除法的思想要了解下。

long long my_gcd(long long a, long long b) {
    return __gcd(a, b); //
    // 辗转相除法
    // return b == 0 ? a : gcd(b, a % b);
}

裴蜀定理
#

设 a 和 b 是两个不全为零的整数。

对于任意整数 x 和 y ,都有:

$$ \gcd(a, b) \mid ax + by $$

也就是说,最大公约数 gcd(a, b) 总是整除形如 ax + by 的表达式。

不仅如此,还存在某些整数 x 和 y ,使得:

$$ ax + by = \gcd(a, b) $$

最小公倍数
#

最小公倍数为 abs(a * b) / __gcd(a, b)

long long lcm(long long a, long long b) {
    return abs(a * b) / __gcd(a, b);
}

素数筛
#

素数筛目的是找出 2 到 n 的所有素数(即质数)。

埃式筛
#

埃式筛的基本思想从 2 到 N 枚举,如果当前数未被标记为合数,则其为素数,并将它的倍数全部标记为合数

vector<bool> sieve_of_eratosthenes(int n) {
    vector<bool>is_prime(n + 1, true);
    // 0 和 1 不是素数
    is_prime[0] = false;
    is_prime[1] = false;
    for(int i = 2; i * i <= n; i++)
        if (is_prime[i])
            for (int j = i * i; j <= n; j += i)
                is_prime[j] = false;
    return is_prime;
}

时间复杂度:

$$O\left(n \log \log n\right)$$

线性筛
#

相比埃式筛多维护一个已确定的质数数组

vector<bool> sieve_of_eratosthenes(int n) {
    vector<bool> is_prime(n + 1, true); 
    vector<int> primes; 
    is_prime[0] = is_prime[1] = false; 
    for (int i = 2; i <= n; i++) {
        if (is_prime[i]) {
            primes.push_back(i);
        }
        for (int p : primes) {
            if (i * p > n) break; // 超出范围
            is_prime[i * p] = false; // 标记 i * p 为非素数
            if (i % p == 0) break;   // 保证每个数只被其最小质因数筛选一次
        }
    }

    return is_prime; // 返回布尔数组
}

时间复杂度:

$$O(n)$$

质因数分解
#

获取小于 n 的质因数集合。

  • 质因数:能整除 n 的质数。

基本思想如下:

  • n 的质因数最大不会超过 sqrt(n)。
  • 从小到大找 n 的因数,每找到一个就将 n 反复除去该因数。
vector<long long> primeDivide(long long n) {
    vector<long long>ans;
    for (long long i = 2; i * i <= n; i++) {
        if (n % i == 0) {
            ans.push_back(i);
            // 如果要统计每个质因数的个数要在此处计数
            while (n % i == 0) {
                n /= i;
            }
        }
    }
    // n 本身是质数 要特判
    if (n > 1) {
        ans.push_back(n);
    }
    return ans;
}

时间复杂度:

$$O(\sqrt{n})$$

例题:

蓝桥杯 完全平方数 - 完全平方数的每个质因数的指数都是偶数

CSP认证 因子化简 - 质因数板子加简单判断

欧拉函数
#

不超过 n 并且和 n 互为质数的个数

  • 互质:n 不为 m 的因数,m 也不为 n 的因数。

上面的质因数分解得到了 n 因数中的质数

根据下面的欧拉函数:

$$\phi(n) = n \prod_{p \mid n} \left( 1 - \frac{1}{p} \right)$$

$$\phi(n) = n \left( 1 - \frac{1}{p_1} \right) \left( 1 - \frac{1}{p_2} \right) \cdots \left( 1 - \frac{1}{p_k} \right)$$

long long euler(long long n, long long MOD) {
    // 1 的欧拉函数是 0
    if (n == 1)
        return 0;
    vector<long long>factor;
    factor = primeDivide(n);
    // 如果 n 本身是质数,注意不能用 size 为 1 判断
    if (factor[0] == n) {
        return (n - 1) % MOD;
    }
    long long ans = n;
    for (auto &i : factor) {
        // 欧拉函数的公式
        ans = ans * (i - 1) / i % MOD; // 注意要这样写才能不触发向下取整
    }
    return ans;
}

时间复杂度:

$$O(\sqrt{n})$$

例题:

蓝桥杯 互质数的个数 - 欧拉函数板子

逆元法求组合数
#

费马小定理:

若 p 是素数,且 a 是整数,满足 \( p \nmid a \) 即 a 不能被 p 整除,则有:

$$ a^{p-1} \equiv 1 \pmod{p} \ $$

组合数公式:

$$ \text{comb}(n, m) \equiv \frac{n!}{m!(n - m)!} \pmod{\text{MOD}} $$

$$ \text{comb}(n, m) \equiv \text{fact}[n] \cdot \text{inv}[m] \cdot \text{inv}[n - m] \pmod{\text{MOD}} $$

$$ 其中 \ \text{inv}[i] = \left( \text{fact}[i] \right)^{-1} \bmod \text{MOD} $$

根据费马小定理:

$$ inv[i] = fact[i]^{-1} \equiv fact[i]^{\text{MOD} - 2} \pmod{\text{MOD}} $$

此外有逆元递推式:

$$\text{inv}[i] = \text{inv}[i + 1] \times (i + 1) \bmod \text{MOD} \quad $$

const int MOD = 998244353; // 需要保证 MOD 是质数
const int MAXN = 2 * 100000 + 10;

vector<long long> inv(MAXN, 1);
vector<long long> fact(MAXN, 1);

// 快速幂
long long qpow(long long i, long long n, long long p) {
    i %= p;
    long long res = 1;
    while (n) {
        if (n & 1) {
            res = (res * i) % p;
        }
        i = (i * i) % p;
        n >>= 1;
    }
    return res;
}
// On 求阶乘数组
for (int i = 1; i < MAXN; i++) {
    fact[i] = (fact[i - 1] * i) % MOD;
}
// On 求逆元数组,inv[MAXN - 1] 用费马小定理
inv[MAXN - 1] = qpow(fact[MAXN - 1], MOD - 2, MOD);
for (int i = MAXN - 2; i >= 0; i--) { // 递推求逆元 
    inv[i] = inv[i + 1] * (i + 1) % MOD;
}

// 也可以直接快速幂求
// inv[i] = qpow(fact[i], MOD - 2, MOD)

// 组合数公式,分母两个阶乘变成逆元
long long comb(int m, int n) {
    return fact[m] * (inv[n] % MOD) % MOD * (inv[m - n] % MOD) % MOD;
}

python 的 pow 自带快速幂和逆元,能大大减小代码量(但 comb 对大数支持并不好)

MOD = 998244353
MAXN = 2 * 10**5 + 10

fact = [1] * MAXN
inv = [1] * MAXN

for i in range(1, MAXN):
    fact[i] = fact[i - 1] * i % MOD

inv[MAXN - 1] = pow(fact[MAXN - 1], MOD - 2, MOD)

for i in range(MAXN - 2, -1, -1):
    inv[i] = inv[i + 1] * (i + 1) % MOD

# 组合数 C(n, k)
def comb(n, k):
    if k < 0 or k > n:
        return 0
    return fact[n] * inv[k] % MOD * inv[n - k] % MOD

相关文章

“二分查找” 总结与例题
·3279 字·7 分钟
算法与数据结构 二分 模板
算法题 - 分类索引
·7720 字·16 分钟
算法与数据结构
从记忆化搜索到动态规划
·645 字·2 分钟
算法与数据结构 记忆化搜索 动态规划
从“猫和老鼠”题解看博弈题型
·3540 字·8 分钟
算法与数据结构 博弈 动态规划 图论 拓扑排序