NeFut Logo NeFut
EN 管理员登录

深入探讨卡特兰数与斯特林数的动态规划实现

发布于:2026-05-27 09:17 最后更新:2026-06-08 08:56
#C++ #Tutorial

核心逻辑与数学原理

1. 卡特兰数(Catalan Number)

卡特兰数是组合数学中最常见的特殊计数序列之一。记第 $n$ 项为 $H_n$,其标准通项公式为:

$$H_n = \frac{C_{2n}^n}{n + 1} = C_{2n}^n - C_{2n}^{n-1}$$

其通项满足以下高效的代数递推式:

$$H_n = \frac{4n - 2}{n + 1} H_{n-1} \, (H_0 = 1)$$

格路模型证明

在平面直角坐标系中,从 $(0,0)$ 出发,每次只能向右或向上移动 $1$ 个单位,求走到 $(n,n)$且途径任意点 $(x,y)$ 时均满足 $x \geq y$(即不穿过直线 $y=x$)的非降路径数。

2. 第一类斯特林数(Stirling Number of the First Kind)

具有符号的第一类斯特林数 $\begin{bmatrix} n \\ k \end{bmatrix}$(记作 $s(n,k)$),表示将 $n$ 个不同的元素构成 $k$ 个互不相交的圆排列(环)的方案数。其状态组合递推逻辑为:

$$\begin{bmatrix} n \\ k \end{bmatrix} = \begin{bmatrix} n-1 \\ k-1 \end{bmatrix} + (n-1) \times \begin{bmatrix} n-1 \\ k \end{bmatrix}$$

3. 第二类斯特林数(Stirling Number of the Second Kind)

第二类斯特林数 $\begin{Bmatrix} n \\ k \end{Bmatrix}$(记作 $S(n,k)$),表示将 $n$ 个不同的元素拆分成 $k$ 个非空且无区别的子集(集合)的方案数。其状态合并递推逻辑为:

$$\begin{Bmatrix} n \\ k \end{Bmatrix} = \begin{Bmatrix} n-1 \\ k-1 \end{Bmatrix} + k \times \begin{Bmatrix} n-1 \\ k \end{Bmatrix}$$


状态设计与算法推导

在 NOIP 实战中,对于斯特林数这种具备明显二维依存特征的计数,通常采用动态规划二维填表法处理。

1. 第二类斯特林数状态推导逻辑分析

考虑第 $n$ 个元素(新加入元素)的去向:

2. 第一类斯特林数状态推导逻辑分析

考虑第 $n$ 个元素在圆排列中的去向:


C++ 标准源码

#include <iostream>
#include <vector>

const int MOD = 1000000007;
const int MAXN = 2000;

long long catalan[MAXN + 5];
long long S2[MAXN + 5][MAXN + 5];
long long S1[MAXN + 5][MAXN + 5];

// 预处理卡特兰数线性递推
void init_catalan(int n) {
    catalan[0] = 1;
    catalan[1] = 1;
    // 借助费马小定理求分母逆元进行单点线性推导
    auto quick_power = [](long long base, long long exp) {
        long long res = 1;
        while (exp > 0) {
            if (exp & 1) res = (res * base) % MOD;
            base = (base * base) % MOD;
            exp >>= 1;
        }
        return res;
    };

    for (int i = 2; i <= n; ++i) {
        long long inv = quick_power(i + 1, MOD - 2);
        catalan[i] = (4 * i - 2) * catalan[i - 1] % MOD * inv % MOD;
    }
}

// 预处理两类斯特林数矩阵
void init_stirling(int n) {
    S1[0][0] = 1;
    S2[0][0] = 1;

    for (int i = 1; i <= n; ++i) {
        S1[i][0] = 0;
        S2[i][0] = 0;
        for (int j = 1; j <= i; ++j) {
            // 第一类斯特林数状态转移
            S1[i][j] = (S1[i - 1][j - 1] + (i - 1) * S1[i - 1][j]) % MOD;
            // 第二类斯特林数状态转移
            S2[i][j] = (S2[i - 1][j - 1] + j * S2[i - 1][j]) % MOD; // 致命点:此处乘数是 j 而非 i-1
        }
    }
}

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

    init_catalan(MAXN);
    init_stirling(MAXN);

    int n, k;
    if (std::cin >> n >> k) {
        if (n <= MAXN && k <= n) {
            std::cout << "Catalan(" << n << ") = " << catalan[n] << "\n";
            std::cout << "S1(" << n << "," << k << ") = " << S1[n][k] << "\n";
            std::cout << "S2(" << n << "," << k << ") = " << S2[n][k] << "\n";
        }
    }
    return 0;
}

NOIP 实战避坑指南

  1. 混淆一类与二类斯特林数的转移乘数 选手在紧张时经常会记错乘数。第一类乘的是整个前驱规模 $(i - 1)$(代表可插入的位置数),而第二类乘的是当前目标集合数 $j$(代表可选择的合集数)。一旦两数记反,递推表的数据特征会发生根本性突变。
  2. 卡特兰数线性递推中忘记求逆元导致整除截断 在公式 $H_n = \frac{4n-2}{n+1} H_{n-1}$ 中,涉及分子除以 $n+1$。在模意义下,直接使用 C++ 的 / 运算符相当于对中间结果直接抹去小数,而非取模除法。必须老老实实通过快速幂算出 $(n + 1)$ 在模 $MOD$ 意义下的逆元再执行乘法。

经典 NOIP/洛谷 真题

1. 洛谷 P3941 入阵曲 / 变式 洛谷 P5386 [CERC2018] Game on Tree (涉及第二类斯特林数)

2. 洛谷 P1655 小朋友的球

原文链接: local://20.4

[h] 返回首页