帕斯卡三角(杨辉三角)

递推构造与组合数应用

基本定义与递推公式

帕斯卡三角(法国数学家帕斯卡发现),在中国也称为杨辉三角(宋代数学家杨辉整理记载),是一个由数字构成的三角形阵列,在组合数学、概率论等领域有广泛应用。

递推关系

设第 n 行(从 0 开始计数)第 k 列(从 0 开始计数)的数为 C(n, k)。它满足以下递推关系:

C(n, k) = C(n-1, k-1) + C(n-1, k)

边界条件:

C(n, 0) = 1  (每行第一个数)
C(n, n) = 1  (每行最后一个数)

这个公式的意义是:三角形中任意一个非边界位置的数,都等于它“肩膀”上两个数(上一行正上方和左上方)之和。

组合数意义

从组合数学的角度看,C(n, k) 正是组合数,表示从 n 个不同的元素中无序地选取 k 个元素的方案数。

C(n, k) = n! / (k! * (n-k)!)

其中 '!' 表示阶乘 (例如 5! = 5×4×3×2×1)。

注意:在帕斯卡三角中,我们通常从第 0 行开始计数。

递推演示(前5行)

动态生成帕斯卡三角

利用递推公式 C(n, k) = C(n-1, k-1) + C(n-1, k) 和边界条件 C(n, 0) = C(n, n) = 1,我们可以逐行生成任意指定行数的帕斯卡三角。

构建过程解析

  1. 第 0 行:只有一个元素 1。
  2. 后续第 i 行:
    • 行的开头 (j=0) 和结尾 (j=i) 总是 1。
    • 对于行内的其他位置 (0 < j < i),其值为上一行 (i-1) 的第 (j-1) 个元素与第 j 个元素之和。
  3. 重复此过程,直到生成所需的 N 行 (即 0 到 N-1 行)。

C++ 实现帕斯卡三角

下面是使用 C++ `vector` 来生成帕斯卡三角前 `numRows` 行的核心代码片段。这种方法直接利用了递推关系。

// 函数:生成帕斯卡三角的前 numRows 行
// 返回一个二维向量,表示整个三角形
vector> generate(int numRows) {
    vector> triangle; // 用于存储结果的二维向量

    // 如果请求的行数为 0,则返回空三角形
    if (numRows <= 0) {
        return triangle;
    }

    // 逐行构建三角形
    for (int i = 0; i < numRows; ++i) {
        // 创建当前行,大小为 i+1,并用 1 初始化所有元素
        // 因为 C(i, 0) 和 C(i, i) 总是 1
        vector row(i + 1, 1);

        // 计算当前行内部的元素 (从第 1 个到倒数第 2 个)
        // j 从 1 开始,直到 row.size() - 2 (即 i - 1)
        for (int j = 1; j < i; ++j) {
            // 应用递推公式: C(i, j) = C(i-1, j-1) + C(i-1, j)
            // triangle[i-1] 是上一行
            row[j] = triangle[i - 1][j - 1] + triangle[i - 1][j];
        }

        // 将构建好的当前行添加到结果三角形中
        triangle.push_back(row);
    }

    return triangle; // 返回完整的帕斯卡三角
}

算法复杂度分析

  • 时间复杂度:O(N²),其中 N 是要生成的行数 `numRows`。我们需要计算大约 N²/2 个内部元素,每个元素的计算是 O(1) 的。
  • 空间复杂度:O(N²),因为我们需要存储整个 N×N (近似) 大小的三角形。

注意:对于只需要计算某一行或某个特定组合数 C(n, k) 的情况,存在空间复杂度更优的 O(N) 或 O(k) 的算法(如下一页练习题所示)。

组合数查询 & 应用场景

组合数查询 C(n, k)

(下方将显示包含 C(n, k) 的帕斯卡三角部分)

主要应用场景

1. 二项式定理系数

二项式 (a + b)ⁿ 展开后的各项系数,恰好是帕斯卡三角的第 n 行(从第 0 行开始计数)。

(a + b)ⁿ = C(n,0)aⁿb⁰ + C(n,1)aⁿ⁻¹b¹ + C(n,2)aⁿ⁻²b² + ... + C(n,n)a⁰bⁿ

2. 组合计数:路径问题

在一个网格图中,从左上角 (0,0) 走到右下角 (m, n),如果每次只能向右或向下移动一步,那么总共有多少条不同的路径?

答案是 C(m+n, m) 或 C(m+n, n)。这相当于在总共 m+n 步中选择 m 步向右(或 n 步向下)。

例如,从 (0,0) 到 (2,3),共需 2+3=5 步,其中 2 步向右,3 步向下。路径数 = C(5, 2) = C(5, 3) = 10。

3. 概率论:二项分布

在进行 n 次独立的伯努利试验(每次试验只有成功/失败两种结果,成功概率为 p),恰好有 k 次成功的概率 P(X=k) 计算公式中包含组合数:

P(X = k) = C(n, k) * pᵏ * (1-p)ⁿ⁻ᵏ

例如,抛掷一枚均匀硬币 5 次,恰好出现 2 次正面的概率 = C(5, 2) * (0.5)² * (0.5)³ = 10 * 0.25 * 0.125 = 0.3125。

练习题与知识检验

知识测验(选择题)

编程练习

练习1:优化空间计算组合数

请实现一个 C++ 函数 long long getCombination(int n, int k),要求只使用 O(k) 或 O(min(k, n-k)) 的额外空间来计算并返回组合数 C(n, k) 的值。注意处理中间结果可能产生的溢出问题(使用 long long)。

提示:可以利用公式 C(n, k) = C(n-1, k-1) + C(n-1, k) 进行迭代,但只保留上一行的信息,或者利用乘法公式 C(n, k) = (n * (n-1) * ... * (n-k+1)) / k! 并优化计算顺序。

练习2:打印指定行 (O(1) 空间)

请实现一个 C++ 函数 void printPascalRow(int n),要求在仅使用 O(1) 额外空间的条件下(不计算存储最终结果的打印输出空间),打印出帕斯卡三角的第 n 行(从第 0 行算起)。

提示:利用 C(n, k) = C(n, k-1) * (n - k + 1) / k 的关系式,可以在 O(n) 时间内计算并打印第 n 行的每个元素。

实现与应用中的注意事项

潜在问题与解决方案

  • 整数溢出 (Integer Overflow)

    帕斯卡三角中的数值增长非常快。例如,C(30, 15) 就已经超过了 32 位整型 (int) 的最大值。当 n 稍大时(如 N > 30 左右),即使是 64 位整型 (long long 在 C++) 也可能溢出 (例如 C(67, 33) 就会溢出)。

    解决方案:

    • 对 N 较小(如 N <= 30)的情况,使用 long long 类型存储结果。
    • 对 N 更大的情况,如果需要精确值,必须使用**大数运算库**(如 C++ 的 Boost.Multiprecision 或手写大数类)。
    • 如果在算法竞赛或特定场景下,题目要求对一个**模数 M** 取模,则需要在计算过程中应用模运算规则,并可能需要计算乘法逆元(特别是当 M 是素数时)。
    // 示例:使用 long long
    long long combination(int n, int k) {
        if (k < 0 || k > n) return 0;
        if (k == 0 || k == n) return 1;
        if (k > n / 2) k = n - k; // 优化:利用 C(n,k) = C(n, n-k)
    
        long long res = 1;
        for (int i = 1; i <= k; ++i) {
            // 优化乘除顺序防止中间溢出和保证整除
            res = res * (n - i + 1) / i;
            // 注意:这里需要严格证明 res * (n - i + 1) 不会溢出 long long
            // 更安全的做法是先除以 gcd(res, i),再乘 (n-i+1),再除以 i/gcd(res,i)
            // 或者直接使用大数库
        }
        return res;
    }
  • 输入有效性检查

    在计算 C(n, k) 时,务必确保输入满足基本条件:0 ≤ k ≤ nn ≥ 0。对于非法输入(如 k < 0 或 k > n),应返回 0 或抛出异常。

  • 效率考量

    如果需要频繁查询同一个 n 下的不同 k 对应的组合数,预先计算并存储帕斯卡三角的对应行(或整个三角,如果空间允许)会比每次独立计算 C(n, k) 更高效。

计算 C(n, k) 的优化技巧

  • 对称性:利用 C(n, k) = C(n, n-k),总是计算 k 和 n-k 中较小的一个,可以减少计算量。
  • 递推(滚动数组):如练习题1所示,可以用 O(k) 或 O(n) 的空间计算 C(n, k) 或生成一行/整个三角。
  • 乘法公式与顺序优化:如上面代码示例所示,使用 C(n,k) = (n/1) * ((n-1)/2) * ... * ((n-k+1)/k) 的形式,并注意计算顺序以避免中间溢出和浮点数误差(尽量保持整数运算)。
  • 对数计算:对于非常大的 n 和 k,如果只需要近似值或比较大小,可以计算 log(C(n,k)) = log(n!) - log(k!) - log((n-k)!),利用对数将乘除变为加减,并使用 Stirling 公式近似 log(n!)。
  • Lucas 定理:用于快速计算 C(n, k) mod p,其中 p 是素数。

知识延伸:更多奥秘与应用

帕斯卡三角的有趣性质与变种

  • 行和 (Row Sums)

    第 n 行的所有数字之和等于 2ⁿ。这对应于从 n 个元素中选取任意数量(0 到 n 个)元素的总方法数,即集合的幂集大小。

  • 奇偶性与分形 (Parity & Fractals)

    如果将帕斯卡三角中的奇数涂色、偶数留白,当行数足够多时,会形成一个著名的分形图案——**谢尔宾斯基三角 (Sierpinski Triangle)**。

  • 模 p (Modulo p)

    将帕斯卡三角中的每个数对一个素数 p 取模,也会产生有趣的分形结构,这与 Lucas 定理相关。

  • 对角线和 (Diagonal Sums)

    沿对角线方向求和会得到**斐波那契数列 (Fibonacci Numbers)**。

相关组合数与高级应用

  • 卡特兰数 (Catalan Numbers)

    卡特兰数 Cₙ 是一个重要的组合数列,出现在许多计数问题中(如合法括号序列、二叉树形态数、凸多边形三角剖分数等)。它与中心二项式系数有关:Cₙ = (1 / (n+1)) * C(2n, n)。

  • 多项式系数 (Multinomial Coefficients)

    帕斯卡三角对应于二项式 (a+b)ⁿ 的展开系数。对于多项式 (a₁ + a₂ + ... + aₘ)ⁿ 的展开,其系数是多项式系数,可以看作是帕斯卡三角在高维的推广(帕斯卡棱锥/金字塔)。

  • 生成函数 (Generating Functions)

    帕斯卡三角的第 n 行是函数 (1+x)ⁿ 展开式的系数。利用生成函数是解决复杂组合计数问题的强大工具。

算法优化再探

  • 滚动数组 (Rolling Array)

    在动态规划或生成帕斯卡三角时,如果只需要当前行和上一行的数据,可以使用两个一维数组(或一个二维数组的两行)交替存储,将空间复杂度从 O(N²) 优化到 O(N)。

  • 预计算阶乘和逆元 (Precomputation)

    在需要大量计算组合数 C(n, k) mod P(P 为素数)的场景下,可以预先计算 0! 到 N! 的阶乘值及其模 P 的乘法逆元。这样,每次查询 C(n, k) mod P 只需要 O(1) 的时间,通过公式 C(n,k) = n! * inv(k!) * inv((n-k)!) mod P 计算。

帕斯卡三角蕴含着丰富的数学美和深刻的组合思想,值得进一步探索!