Skip to content

Latest commit

 

History

History
925 lines (886 loc) · 28.3 KB

File metadata and controls

925 lines (886 loc) · 28.3 KB

数学问题

1.简单数学

【PAT B1019/A1069】数学黑洞

**给定任一个各位数字不完全相同的 4 位正整数,如果我们先把 4 个数字按非递增排序,再按非递减排序,然后用第 1 个数字减第 2 个数字,将得到一个新的数字。一直重复这样做,我们很快会停在有“数字黑洞”之称的 6174,这个神奇的数字也叫 Kaprekar 常数。 例如,我们从6767开始,将得到

7766 - 6677 = 1089
9810 - 0189 = 9621
9621 - 1269 = 8352
8532 - 2358 = 6174
7641 - 1467 = 6174
... ...

现给定任意 4 位正整数,请编写程序演示到达黑洞的过程。 输入给出一个 (0,104) 区间内的正整数 N。 如果 N 的 4 位数字全相等,则在一行内输出 N - N = 0000;否则将计算的每一步在一行内输出,直到 6174 作为差出现,输出格式见样例。注意每个数字按 4 位数格式输出。**

#include <cstdio>
#include <algorithm>
using namespace std;
bool cmp(int a, int b) { // 递减排序 cmp
    return a > b;
}
void to_array(int n, int num[]) { // 将 n 的每一位存到 num 数组中
    for (int i = 0; i < 4; i++) {
        num[i] = n % 10;
        n /= 10;
    }
}
int to_number(int num[]) { // 将 num 数组转换为数字
    int sum = 0;
    for (int i = 0; i < 4; i++) {
        sum = sum * 10 + num[i];
    }
    return sum;
}
int main() {
    int n, MIN, MAX;
    int num[5]; // 声明数组,用于存储数字的每一位(实际使用4位,多一位用于安全)
    scanf("%d", &n);
    while (1) {
        to_array(n, num); // 将 n 转换为数组
        sort(num, num + 4); // 对 num 数组从小到大排序
        MIN = to_number(num); // 获取最小值
        sort(num, num + 4, cmp); // 对 num 数组从大到小排序
        MAX = to_number(num); // 获取最大值
        n = MAX - MIN; // 计算差值
        printf("%04d - %04d = %04d\n", MAX, MIN, n);
        if (n == 0 || n == 6174) break; // 如果差值为0或6174则退出循环
    }
    return 0;
}

2.最大公约数,最小公倍数(骨传导和李春梅(?)

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

![[Pasted image 20251109131713.png]]

48 ÷ 18 = 212
18 ÷ 12 = 16  
12 ÷ 6 = 20
∴ gcd(48, 18) = 6
int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}

或者

int gcd(int a, int b) {
    return !b ? a : gcd(b, a % b);
}

最小公倍数(lcm)

![[Pasted image 20251109133511.png]]

int lcm(int a, int b) {
    // 使用公式:LCM(a, b) = a * b / GCD(a, b)
    // 先除后乘防止溢出
    return a / gcd(a, b) * b;
}

防止溢出:

int lcm(int a, int b) {
    if (a == 0 || b == 0) return 0;
    // 防止溢出:先除后乘
    int g = gcd(a, b);
    return (a / g) * b;  // 确保除法无余数
}

力扣2447. 最大公因数等于K的子数组数目

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 nums 的子数组中元素的最大公因数等于 k 的子数组数目。 子数组 是数组中一个连续的非空序列。 数组的最大公因数 是能整除数组中所有元素的最大整数。 示例 1: **输入:**nums = [9,3,1,2,6,3], k = 3 **输出:**4 **解释:**nums 的子数组中,以 3 作为最大公因数的子数组如下:

  • [9,3,1,2,6,3]
  • [9,3,1,2,6,3]
  • [9,3,1,2,6,3]
  • [9,3,1,2,6,3] 枚举所有子数组,计算每个子数组的 GCD,并检查是否等于 k 来实现。为了提高效率,可以在枚举时进行优化(剪枝):如果当前 GCD 已经小于 k 且 k>0(继续扩展子数组,GCD 只会更小或保持不变),或者当前 GCD 不为 0 且 k=0,则提前终止内层循环。 PS:对于任意子数组 nums[i..j],扩展为 nums[i..j+1] 时:
gcd(nums[i..j+1]) = gcd(gcd(nums[i..j]), nums[j+1])

由于 gcd(a, b) 总是整除 a,所以:

gcd(nums[i..j+1]) ≤ gcd(nums[i..j])

即 GCD 序列是单调不增的。 因此,一旦 g < k,后续的 GCD 值只会 ≤ g,永远不可能 = k

主函数 subarrayGCD

  • 遍历每个起始索引 i
  • 对于每个起始索引 i,初始化 g 为 nums[i],然后遍历从 i 到数组末尾的子数组。
  • 更新当前子数组的 GCD g,如果 g 等于 k,则增加计数。
  • 根据 k 的值进行优化:如果 k>0 且 g 小于 k,或者 k=0 且 g 不为 0,则提前终止内层循环。 代码块(暴力)
class Solution {
public:
    int gcd(int a, int b) {
       return !b ? a : gcd(b, a % b);
    }
    int subarrayGCD(vector<int>& nums, int k) {
        int n = nums.size();
        int count = 0;
        for (int i = 0; i < n; i++) {
            int g = nums[i];
            for (int j = i; j < n; j++) {
                g = gcd(g, nums[j]);
                if (g == k) {
                    count++;
                }
                // 剪枝:如果当前 GCD 已经不可能等于 k,提前终止循环
                if (k > 0 && g < k) {
                    break;
                }
                if (k == 0 && g != 0) {
                    break;
                }
            }
        }
        return count;
    }
};

力扣2470. 最小公倍数等于K的子数组数目:

LCM 序列具有单调不减的性质,这与 GCD 的单调不增性质形成对比。

class Solution {
public:
    int gcd(int a, int b){
        return !b ? a : gcd(b, a % b);
    }
    int lcm(int a, int b){
        if (a == 0 || b == 0) return 0;
        return a / gcd(a, b) * b;
    }
    int subarrayLCM(vector<int>& nums, int k) {
        int n = nums.size();
        int count = 0;
        for(int i = 0; i < n; i++){
            //初始化
             int l = nums[i];
             for(int j = i;  j < n; j++){
                l = lcm(l, nums[j]);
                if(l > k){
                    break;
                }
                if(l == k){
                    count++;
                }
             }
        }
        return count;
    }
};

为了防溢出,可以使用longlong

总结一下这两道题

  • 这两道题几乎一模一样,区别在于,GCD是单调不增的,LCM是单调不减的(用于剪枝)
  • 其次,GCD和LCM函数必须掌握!!先写这两个函数
  • 主函数:主要思路是找一个”引子“,开始遍历。从第一个元素开始,把这个元素设为引子。然后遍历剩下的节点
  • 剪枝:体现在两方面。 一个是以g(l)为引子,遍历剩下的元素时重新定义g(l),使得g为遍历过的元素的最大公约数/最小公倍数。 另一个是利用GCD,LCM的性质进行剪枝(单调不增,单调不减)

3.分数

分数的表示

struct Fraction {
    int up, down;    // 分子、分母
};

规则:

  • 分母为非负数,分数为负时只让分子为负
  • 分数为0时,分子为0,分母为1
  • 分子分母互质(没有除了1以外的公约数)

分数的化简

Fraction reduction(Fraction result) {
    if(result.down < 0) {
        result.up = -result.up;
        result.down = -result.down;
    }
    if(result.up == 0) {
        result.down = 1;
    } else {
        int d = gcd(abs(result.up), abs(result.down));
        result.up /= d;
        result.down /= d;
    }
    return result;
}

分数的四则运算

加法:

result = (f1.up*f2.down + f2.up*f1.down) / (f1.down*f2.down)

减法:

result = (f1.up*f2.down - f2.up*f1.down) / (f1.down*f2.down)

乘法:

result = (f1.up*f2.up) / (f1.down*f2.down)

除法:

result = (f1.up*f2.down) / (f1.down*f2.up)

注意: 除法要判断除数是否为0

分数的输出规则

  1. 先化简分数
  2. 分母为1 → 输出整数
  3. 假分数 → 输出带分数形式
  4. 真分数 → 原样输出

力扣592. 分数加减运算

给定一个表示分数加减运算的字符串 expression ,你需要返回一个字符串形式的计算结果。  这个结果应该是不可约分的分数,即 最简分数。 如果最终结果是一个整数,例如 2,你需要将它转换成分数形式,其分母为 1。所以在上述例子中, 2 应该被转换为 2/1示例 : 输入: expression = "-1/2+1/2" 输出: "0/1" isdigit :C++ 标准库中的一个函数,用于判断一个字符是否是数字字符。 代码块:(longlong防溢出)

class Solution {
public:
    string fractionAddition(string expression) {
        // 初始化结果为 0/1
        long long up = 0, down = 1;
        
        int i = 0, n = expression.size();
        while (i < n) {
            // 读取符号
            int sign = 1;
            if (expression[i] == '+' || expression[i] == '-') {
                if (expression[i] == '-') sign = -1;
                i++;
            }
            
            // 读取分子
            long long num = 0;
            while (i < n && isdigit(expression[i])) {
                num = num * 10 + (expression[i] - '0');//可能是多位数
                i++;
            }
            num *= sign;
            
            // 跳过 '/'
            i++;
            
            // 读取分母
            long long den = 0;
            while (i < n && isdigit(expression[i])) {
                den = den * 10 + (expression[i] - '0');
                i++;
            }
            
            // 通分相加: a/b + c/d = (a*d + c*b)/(b*d)
            up = up * den + num * down;
            down = down * den;
            
            // 每次计算后化简,防止溢出
            long long g = gcd(abs(up), down);
            up /= g;
            down /= g;
        }
        
        return to_string(up) + "/" + to_string(down);
    }

private:
    // 最大公约数函数
    long long gcd(long long a, long long b) {
        while (b != 0) {
            long long temp = b;
            b = a % b;
            a = temp;
        }
        return a;
    }
};

力扣166. 分数到小数

给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以 字符串形式返回小数  如果小数部分为循环小数,则将循环的部分括在括号内。 如果存在多个答案,只需返回 任意一个 。 对于所有给定的输入,保证 答案字符串的长度小于 104 。 注意,如果分数可以表示为有限长度的字符串,则 必须 返回它。 示例 : **输入:**numerator = 1, denominator = 2 输出:"0.5" 代码块: llabs计算 long long 类型的绝对值。

class Solution {
public:
    string fractionToDecimal(int numerator, int denominator) {
        if (numerator == 0) return "0";
        string result;
        // 处理符号
        if ((numerator < 0) ^ (denominator < 0)) {
            result += '-';
        }
        // 转换为正数处理,使用long long防止溢出
        long long num = llabs((long long)numerator);
        long long den = llabs((long long)denominator);
        // 整数部分
        result += to_string(num / den);
        num %= den;
        if (num == 0) return result;
        // 小数部分
        result += '.';
        unordered_map<long long, int> remainderPos;
        while (num != 0) {
            // 如果当前余数已经出现过,说明开始循环
            if (remainderPos.find(num) != remainderPos.end()) {
                result.insert(remainderPos[num], "(");
                result += ')';
                break;
            }  
            // 记录当前余数的位置
            remainderPos[num] = result.size();
            
            // 计算下一位小数
            num *= 10;
            result += to_string(num / den);
            num %= den;
        }
        return result;
    }
};

4.素数

思路

  • 素数定义:只能被1和自身整除的大于1的自然数
  • 关键优化:只需检查到 √n 即可,因为如果n有因数,必有一个 ≤ √n

两种实现方式

方法一:开根号法(推荐) sqrt : 计算n的平方根,并向下取整(转换为整数)。接受一个double类型的参数,并返回double类型的平方根。

bool isPrime(int n) {
    if (n <= 1) return false;
    int sqr = (int)sqrt(1.0 * n);
    for (int i = 2; i <= sqr; i++) {                       
        if (n % i == 0) return false;
    }
    return true;
}

方法二:平方判断法

bool isPrime(int n) {
    if (n <= 1) return false;
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) return false;
    }
    return true;
}

素数表(pNum初始值为0)

方法一:逐个判断法

  • 时间复杂度:O(n√n)
  • 适合 n ≤ 10⁵
void findPrime() {
    for (int i = 1; i < maxn; i++) {
        if (isPrime(i)) {
            prime[pNum++] = i;
            p[i] = true;
        }
    }
}

方法二:埃拉托斯特尼筛法(推荐)

  • 时间复杂度:O(nloglogn)
  • 核心思想:用素数标记掉所有倍数
void findPrime() {
    for (int i = 2; i < maxn; i++) {
        if (!p[i]) { // i是素数
            prime[pNum++] = i;//将素数i存入prime数组
            for (int j = i + i; j < maxn; j += i) {
                p[j] = true; // 标记倍数为非素数
            }
        }
    }
}

204. 计数质数

我写的:

class Solution {
public:
    bool isPrimes(int n){
        if(n <= 1) return false;
        int sqr = sqrt(1.0 * n);
        for(int i = 2; i <= sqr; i++){
            if(n % i == 0) return false;
        }
        return true;
    }
    int countPrimes(int n) {
        int ans = 0;
        for(int i = 1; i < n; i++){
            if(isPrimes(i)) ans++;
        }
        return ans;
    }
};

超出了时间限制。 怎么优化? 使用埃拉托斯特尼筛法

  1. 基础版本(从 i+i 开始)
  2. 优化版本(从 i*i 开始)
class Solution {
public:
    int countPrimes(int n) {
        if (n <= 2) return 0;  // 小于2没有素数 
        // 创建标记数组,初始都认为是素数
        vector<bool> isPrime(n, true);
        isPrime[0] = false;  // 0不是素数
        isPrime[1] = false;  // 1不是素数
        // 核心:埃拉托斯特尼筛法
        for (int i = 2; i * i < n; i++) {
            if (isPrime[i]) {  // 如果i是素数
                // 标记i的所有倍数为非素数
                // 从i*i开始,因为所有小于 i² 的 i 的倍数都已经被更小的素数标记过了
                for (int j = i * i; j < n; j += i) {
                    isPrime[j] = false;
                }
            }
        }
        // 统计结果
        int count = 0;
        for (int i = 2; i < n; i++) {
            if (isPrime[i]) count++;
        }
        return count;
    }
};

数学题做的我头大,所以讲个笑话:

提交了 i+i 的解法后,系统说:"Time Limit Exceeded"
质数5叹了口气:"早告诉你要从 i*i 开始优化,你偏不信!" 这些图片内容是关于
质因子分解**的算法讲解和代码实现,主要来自《算法笔记》这本书。我来为你系统性地梳理和讲解:

5. 质因子分解的基本概念

质因子分解:将一个正整数分解为若干个质数(素数)的乘积形式。

表示形式:

  • 连乘形式:180 = 2 × 2 × 3 × 3 × 5
  • 指数形式:180 = 2² × 3² × 5¹ 注意:1不是素数,没有质因子。

数据结构设计

struct factor {
    int x, cnt;    // x为质因子,cnt为其个数(指数)
} fac[10];

为什么数组大小是10? 因为 2×3×5×7×11×13×17×19×23×29 已经超过int范围,所以对于int范围内的数,最多有10个不同的质因子。

思路

对于正整数n,它的质因子要么:

  1. 全部 ≤ √n
  2. 或者只有一个 > √n,其余都 ≤ √n 作法
  • 枚举 1~√n 范围内的所有质因子p
  • 判断 p是否是n的因子:
    • 如果是:记录该因子,并不断除以p,统计出现次数
    • 如果不是:跳过
  • 检查 如果最后n > 1,说明n本身就是一个质因子

代码块(PAT A1059)

#include <cstdio>
#include <cmath>
const int maxn = 100010;
// 判断素数函数
bool is_prime(int n) {
    if(n == 1) return false;
    int sqr = (int)sqrt(1.0 * n);
    for(int i = 2; i <= sqr; i++) {
        if(n % i == 0) return false;
    }
    return true;
}
// 素数表
int prime[maxn], pNum = 0; 
void Find_Prime() {
    for(int i = 1; i < maxn; i++) {
        if(is_prime(i)) {
            prime[pNum++] = i;
        }
    }
}
// 质因子结构体
struct factor {
    int x, cnt;
} fac[10];
int main() {          
    Find_Prime();    // 务必调用!
    int n, num = 0;             
    scanf("%d", &n);
    if(n == 1) printf("1=1");    // 特判1的情况
    else {
        printf("%d=", n);
        int sqr = (int)sqrt(1.0 * n);
        // 枚举√n以内的质因子
        for(int i = 0; i < pNum && prime[i] <= sqr; i++) {
            if(n % prime[i] == 0) {
                fac[num].x = prime[i];
                fac[num].cnt = 0;
                while(n % prime[i] == 0) {
                    fac[num].cnt++;
                    n /= prime[i];
                }
                num++;
            }
            if(n == 1) break;    // 提前退出
        }
        // 处理大于√n的质因子
        if(n != 1) {
            fac[num].x = n;
            fac[num++].cnt = 1;
        }
        // 输出结果
        for(int i = 0; i < num; i++) {
            if(i > 0) printf("*");
            printf("%d", fac[i].x);
            if(fac[i].cnt > 1) printf("^%d", fac[i].cnt);
        }
    }
    return 0;
}

常见错误

  1. 忘记调用 Find_Prime() 函数
  2. 循环条件写错(如 i ≤ maxn 应为 i < maxn
  3. 没有处理大于√n的质因子
  4. 在循环条件中直接计算sqrt(n),应该提前存储

扩展

求因子个数: 如果 n = p₁ᵉ¹ × p₂ᵉ² × ... × pₖᵉᵏ,则: 因子个数 = (e₁+1) × (e₂+1) × ... × (eₖ+1) 求因子和: 因子和 = (1+p₁+p₁²+...+p₁ᵉ¹) × (1+p₂+p₂²+...+p₂ᵉ²) × ... = ∏[(pᵢᵉⁱ⁺¹ - 1)/(pᵢ - 1)] 这些图片内容是关于大整数运算(高精度运算) 的详细讲解,我来为你系统性地梳理:

6.大整数运算

当整数位数超过基本数据类型(如int、long long)的范围时(比如1000位数),就需要用特殊方法处理。

存储方式:

memset 是 C/C++ 中的一个内存设置函数,用于将一块内存区域的每个字节都设置为特定的值。

void *memset(void *str, int c, size_t n)
struct bign {  // big number的缩写
    int d[1000];  // 存储每一位数字
    int len;      // 记录数字长度
    // 构造函数,自动初始化
    bign() {
        memset(d, 0, sizeof(d));// 将d数组的所有字节设为0
        len = 0;
    }
};

重要特点

  • 顺位存储:整数的高位存储在数组的高位,低位存储在数组的低位
  • 例如:235813 → d[0]=3, d[1]=1, d[2]=8, d[3]=5, d[4]=3, d[5]=2
数组索引:  0  1  2  3  4
存储内容:  5  4  3  2  1
数字意义: 个位 十位 百位 千位 万位

字符串转大整数:

bign change(char str[]) {
    // 将字符串形式的数字转换为bign结构体
    // 例如:"12345" → bign{ [5,4,3,2,1], len=5 }
    bign a;//创建bign对象
    a.len = strlen(str);//设置长度
    for(int i = 0; i < a.len; i++) {
        // 逆序存储:字符串是正序,要转为逆序存储
        a.d[i] = str[a.len - i - 1] - '0';
    }
    return a;
}

为什么要逆序储存?

字符串索引: 0   1   2   3   4
字符内容:  '1','2','3','4','5'
数字意义:  万位 千位 百位 十位 个位

大整数比较

int compare(bign a, bign b) {
    if(a.len > b.len) return 1;      // a大
    else if(a.len < b.len) return -1; // a小
    else {
        for(int i = a.len - 1; i >= 0; i--) {
            if(a.d[i] > b.d[i]) return 1;
            else if(a.d[i] < b.d[i]) return -1;
        }
        return 0; // 相等
    }
}

高精度加法(竖式)

  147
+  65
------
  212

代码块:

bign add(bign a, bign b) {
    bign c;
    int carry = 0; // 进位标志,初始为0
    for(int i = 0; i < a.len || i < b.len; i++) {
        int temp = a.d[i] + b.d[i] + carry;//当前位相加 + 进位
        c.d[c.len++] = temp % 10;  // 取个位作为结果
        carry = temp / 10;         // 计算新的进位
    }
    if(carry != 0) {
        c.d[c.len++] = carry;
    }
    return c;
}

高精度减法

  147
-  65
------
   82

代码块:

bign sub(bign a, bign b) {
    bign c;
    
    for(int i = 0; i < a.len || i < b.len; i++) {
        if(a.d[i] < b.d[i]) {          // 不够减
            a.d[i+1]--;               // 向高位借1
            a.d[i] += 10;             // 当前位加10
        }
        c.d[c.len++] = a.d[i] - b.d[i];
    }
    
    // 去除高位多余的0
    while(c.len - 1 >= 1 && c.d[c.len - 1] == 0) {
        c.len--;
    }
    return c;
}

注意:使用前要先比较大小,如果a<b,需要交换并输出负号。

高精度与低精度的乘法

  147
×  35
------
   735
  441
------
  5145

代码块:

bign multi(bign a, int b) {
    bign c;
    int carry = 0;
    for(int i = 0; i < a.len; i++) {
        int temp = a.d[i] * b + carry;
        c.d[c.len++] = temp % 10;
        carry = temp / 10;
    }
    // 处理剩余进位(可能有多位)
    while(carry != 0) {
        c.d[c.len++] = carry % 10;
        carry /= 10;
    }
    return c;
}

高精度与低精度的除法

代码块:

bign divide(bign a, int b, int& r) {  // r为余数(引用传递)
    bign c;
    c.len = a.len;  // 商与被除数位数一一对应
    r = 0;          // 余数初始化为0
    
    for(int i = a.len - 1; i >= 0; i--) {
        r = r * 10 + a.d[i];      // 被除数
        if(r < b) {
            c.d[i] = 0;           // 不够除
        } else {
            c.d[i] = r / b;       //
            r = r % b;            // 新余数
        }
    }
    // 去除高位多余的0
    while(c.len - 1 >= 1 && c.d[c.len - 1] == 0) {
        c.len--;
    }
    return c;
}

输出函数

void print(bign a) {
    for(int i = a.len - 1; i >= 0; i--) {
        printf("%d", a.d[i]);
    }
}

7.组合数(存疑)

计算 n! 中质因子 p 的个数

![[Pasted image 20251109182820.png]]

// 非递归版本
int cal(int n, int p) {
    int ans = 0;
    while (n) {
        ans += n / p;
        n /= p;
    }
    return ans;
}

// 递归版本
int cal(int n, int p) {
    if (n < p) return 0;
    return n / p + cal(n / p, p);
}
  • 应用:例如计算 n! 末尾零的个数(即质因子 5 的个数)。

计算组合数 ( C(n, m) )

组合数![[Pasted image 20251109182854.png]]直接计算阶乘容易溢出,因此有多种方法。

方法一:直接计算定义式

  • 做法:分别计算 n!、m! 和 (n-m)!,然后相除。
  • 缺点:阶乘增长快,仅适用于小数据(n ≤ 20)。
  • 代码
    long long C(long long n, long long m) {
        long long ans = 1;
        for (long long i = 1; i <= n; i++) ans *= i;
        for (long long i = 1; i <= m; i++) ans /= i;
        for (long long i = 1; i <= n - m; i++) ans /= i;
        return ans;
    }

方法二:递推公式

  • 公式:( C(n, m) = C(n-1, m) + C(n-1, m-1) ),边界 ( C(n, 0) = C(n, n) = 1 )。
  • 优点:避免阶乘计算,支持较大 n。
  • 代码(带记忆化):
    long long res[67][67] = {0};
    long long C(long long n, long long m) {
        if (m == 0 || m == n) return 1;
        if (res[n][m] != 0) return res[n][m];
        return res[n][m] = C(n-1, m) + C(n-1, m-1);
    }
  • 递推代码(计算整个表,时间复杂度 (O(n^2))):
    const int n = 60;
    long long res[n+1][n+1] = {0};
    void calC() {
        for (int i = 0; i <= n; i++) {
            res[i][0] = res[i][i] = 1;
            for (int j = 1; j <= i/2; j++) {
                res[i][j] = res[i-1][j] + res[i-1][j-1];
                res[i][i-j] = res[i][j]; // 利用对称性
            }
        }
    }

方法三:定义式的变形

  • 做法:将 ( C(n, m) ) 写为 ( \frac{(n-m+1) \times (n-m+2) \times \cdots \times n}{1 \times 2 \times \cdots \times m} ),边乘边除。
  • 优点:减少溢出风险,时间复杂度 (O(m))。
  • 代码
    long long C(long long n, long long m) {
        long long ans = 1;
        for (long long i = 1; i <= m; i++) {
            ans = ans * (n - m + i) / i; // 先乘后除
        }
        return ans;
    }
  • 注意:在 n=62, m=31 时可能溢出,但比方法一支持的范围大。

计算组合数模 p ![[Pasted image 20251109182948.png]]

当组合数很大时,常需计算模 p 的结果。p 可能是素数或非素数,需不同方法。

方法一:递推公式计算模 p

  • 做法:递推过程中取模。
  • 适用:n, m 较小(如 n ≤ 1000),p 任意。
  • 代码
    int res[1010][1010] = {0};
    int C(int n, int m, int p) {
        if (m == 0 || m == n) return 1;
        if (res[n][m] != 0) return res[n][m];
        return res[n][m] = (C(n-1, m, p) + C(n-1, m-1, p)) % p;
    }

方法二:质因子分解

  • 做法
    1. 分解 n!、m!、(n-m)! 中质因子 p 的个数。
    2. 计算 ( C(n, m) ) 中 p 的个数 c。
    3. 用快速幂计算 ( p^c \mod p )(实际上 c 可能为 0,但这里是对每个质因子单独处理)。
  • 适用:n 较大(如 n ≤ 10^6),p 任意。
  • 代码
    // 假设 prime[] 为素数表
    int C(int n, int m, int p) {
        int ans = 1;
        for (int i = 0; prime[i] <= n; i++) {
            int c = cal(n, prime[i]) - cal(m, prime[i]) - cal(n-m, prime[i]);
            ans = (ans * binaryPow(prime[i], c, p)) % p;
        }
        return ans;
    }

方法三:定义式变形计算模 p(p 是素数)

  • 情况 1:m < p。直接计算,用逆元处理除法。
  • 情况 2:m 可能 ≥ p。统计分子和分母中质因子 p 的个数,去除 p 后计算。
  • 代码(情况 2):
    int C(int n, int m, int p) {
        int ans = 1, numP = 0;
        for (int i = 1; i <= m; i++) {
            int temp = n - m + i;
            while (temp % p == 0) { numP++; temp /= p; }
            ans = (ans * temp) % p;
    
            temp = i;
            while (temp % p == 0) { numP--; temp /= p; }
            ans = (ans * inverse(temp, p)) % p; // inverse 为逆元
        }
        if (numP > 0) return 0; // 分子中 p 更多,结果为 0
        else return ans;
    }
  • 适用:m 较小(如 m ≤ 10^6),p 是素数。

方法四:Lucas 定理(p 是素数)

  • 定理:( C(n, m) \mod p = C(n \mod p, m \mod p) \times C(\lfloor n/p \rfloor, \lfloor m/p \rfloor) \mod p )。
  • 优点:处理 n, m 很大但 p 较小的场景。
  • 代码
    int Lucas(int n, int m, int p) {
        if (m == 0) return 1;
        return (C(n % p, m % p, p) * Lucas(n / p, m / p, p)) % p;
    }
    其中 C(n % p, m % p, p) 可用方法三的情况 1 计算。
  • 适用:p 较小(如 p ≤ 10^6),n 和 m 很大。

4. 当 p 不是素数时的处理

  • 做法
    1. 将 p 分解为质因子幂积:( p = p_1^{k_1} p_2^{k_2} \cdots )。
    2. 对每个 ( p_i^{k_i} ),计算 ( C(n, m) \mod p_i^{k_i} )(用方法三的变形)。
    3. 用中国剩余定理(CRT)合并结果。
  • 例子:计算 ( C(10, 3) \mod 24 )(24=2^3×3)。
    • 分别计算 mod 8 和 mod 3,然后合并。

5. 方法总结与选择

场景 n, m 范围 p 条件 推荐方法
小数据 n, m ≤ 20 任意 直接计算(方法一)
中小数据 n, m ≤ 1000 任意 递推公式(方法一)
n 大、m 小 m ≤ 10^6 任意 质因子分解(方法二)
n、m 大 n, m 大 p 小且为素数 Lucas 定理(方法四)
p 非素数 n, m 任意 p 非素数 质因子分解 + CRT