NeFut Logo NeFut
EN 管理员登录

深入解析二项式定理与杨辉三角的高精度计算

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#algorithm #C++ #Math

核心逻辑与数学原理

1. 二项式定理(Binomial Theorem)

二项式定理阐明了两个数之和的整数次幂展开后的代数结构。对于任意实数 $a, b$ 与非负整数 $n$,其通用展开式为:

$$(a + b)^n = \\sum_{k=0}^n C_n^k a^{n-k} b^k$$

其中 $C_n^k$ 为组合数(即二项式系数)。该公式展示了组合数与多项式展开系数之间的本质映射。

2. 帕斯卡三角形(杨辉三角)的数学性质

将二项式系数 $C_n^k$ 按照行(对应 $n$)和列(对应 $k$)排列,即构成帕斯卡三角形。它具备以下硬核数论性质:

$$C_n^k = C_{n-1}^{k-1} + C_{n-1}^k$$

这是无需除法、仅靠加法构建组合数矩阵的基石。

$$\sum_{i=k}^n C_i^k = C_{n+1}^{k+1}$$

该性质常用于将复杂的单变量累加组合数瞬间坍缩为单项组合数。


算法推导与状态设计

当 NOIP 题目中的组合数计算没有给出模数,且 $n, m$ 的范围允许达到几百到几千时,组合数的真实值将突破 unsigned long long 的承载极限。此时必须使用高精度算法(BigInt)进行状态维护。

杨辉三角高精度状态设计

若直接套用通项公式 $C_n^m = \frac{n!}{m!(n-m)!}$,在高精度环境下需要实现高精度阶乘、高精度乘法以及极其复杂的高精度除法。这在赛时极其容易写挂。

最高效、最稳妥的策略是利用杨辉三角的加法递推性质进行状态转移。定义二维状态矩阵 f[i][j],其类型为自定义的高精度大整数结构体 BigInt。状态转移方程为:

$$\text{f}[i][j] = \text{f}[i-1][j-1] + \text{f}[i-1][j]$$

无后效性边界条件:对于任意 $i$,有 $ ext{f}[i][0] = 1$,且当 $j > i$ 时 $ ext{f}[i][j] = 0$。通过两层循环正向推表,利用纯粹的高精度加法即可在 $O(n^2)$ 的时间复杂度内安全填满目标状态,彻底绕过高精度除法的逻辑陷阱。


C++ 标准源码

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

const int MAXN = 200; // 根据实际非模高精题目的空间需求调整边界

// 极致精简的高精度大整数结构体,专为杨辉三角加法定制
struct BigInt {
    std::vector<int> digits; // 逆序存储每一位数字,即 digits[0] 是个位

    BigInt() { digits.push_back(0); }
    BigInt(long long num) {
        if (num == 0) digits.push_back(0);
        while (num > 0) {
            digits.push_back(num % 10);
            num /= 10;
        }
    }

    // 重载加法运算符:O(L) 复杂度的标准每位进位加法
    BigInt operator+(const BigInt &b) const {
        BigInt res;
        res.digits.clear();
        int carry = 0;
        int i = 0, j = 0;
        int len1 = digits.size(), len2 = b.digits.size();

        while (i < len1 || j < len2 || carry) {
            int sum = carry;
            if (i < len1) sum += digits[i++];
            if (j < len2) sum += b.digits[j++];
            carry = sum / 10;
            res.digits.push_back(sum % 10);
        }
        return res;
    }

    // 输出转换
    void print() const {
        for (int i = digits.size() - 1; i >= 0; --i) {
            std::cout << digits[i];
        }
    }
};

BigInt C[MAXN + 5][MAXN + 5];

void precompute_pascal() {
    for (int i = 0; i <= MAXN; ++i) {
        C[i][0] = BigInt(1); // 边界状态:从 i 个里选 0 个方案数永为 1
        for (int j = 1; j <= i; ++j) {
            C[i][j] = C[i - 1][j - 1] + C[i - 1][j]; // 状态转移
        }
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    precompute_pascal();

    int n, m;
    if (std::cin >> n >> m) {
        if (m < 0 || m > n) {
            std::cout << "0\n";
        } else {
            C[n][m].print();
            std::cout << "\n";
        }
    }
    return 0;
}

NOIP 实战避坑指南

  1. 高精度数字输出时的顺序颠倒错误 为了方便进位处理,高精度 BigInt 在内部 std::vector 中一律采用低位在左(逆序)的方式存储(即 digits[0] 存放个位,digits[1] 存放十位)。很多选手在最终输出时习惯性写成 for(int i=0; i<digits.size(); ++i) std::cout<<digits[i],导致输出数字被完全颠倒,当场爆零。必须从 size()-1 开始反向扫描输出。
  2. 多组询问未对高精向量执行 clear() 重载加法操作符产生新对象时,如果不将内部的 digits 清空,或者在构造函数中多塞了默认的 0,会导致每次做加法都会在高位累加一堆冗余的 0。随着杨辉三角层数的加深,数字长度会因前导零的存在呈指数级虚假膨胀,直接引发内存超限(MLE)或运行超时(TLE)。

经典 NOIP/洛谷 真题

1. 洛谷 P10077 [NOIP2004 提高组] 开心的金明 / 类似数论变式 洛谷 P1313 计算系数

2. 洛谷 P2415 集合求和

原文链接: local://20.3

[h] 返回首页