NeFut Logo NeFut
EN 管理员登录

深入解析组合计数与算法优化

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

核心逻辑与数学原理

组合计数是离散数学的核心。在 NOIP 竞赛中,所有复杂的计数模型最终都可以归结为三大基础支柱:排列组合通项、放球模型(隔板法)以及错位排列。

1. 排列与组合基础公式

$$A_n^m = \frac{n!}{(n-m)!} = n \cdot (n-1) \dots (n-m+1)$$

$$C_n^m = \frac{n!}{m!(n-m)!}$$

组合数满足对称性 $C_n^m = C_n^{n-m}$ 以及递推性质(杨辉三角公式) $C_n^m = C_{n-1}^{m-1} + C_{n-1}^m$。

2. 隔板法(放球模型)

用于解决 $n$ 个相同的球放入 $m$ 个不同的盒子 的计数问题。

$$C_{n-1}^{m-1}$$

$$C_{n+m-1}^{m-1}$$

3. 错位排列(Derangement)

全排列 $$ 满足对于任意的 $i$,都有 $(i) eq i$。记 $n$ 个元素的错位排列数为 $D_n$。其数学递推公式为:

$$D_n = (n-1)(D_{n-1} + D_{n-2})$$

递推逻辑证明
考虑第 $n$ 个元素放到了位置 $k$ (共有 $n-1$ 种选择,其中 $k eq n$)。


状态设计与算法推导

在面对多组询问的线性范围内计数时,直接套用通项公式效率低下。我们通常需要利用状态预处理来达到 $O(1)$ 的响应速度。

$$D[i] = (i - 1) \cdot (D[i - 1] + D[i - 2]) \bmod \text{MOD}$$

该转移仅依赖前驱两项,可在线性扫描中以 $O(N)$ 复杂度填满状态表。


C++ 标准源码

#include <iostream>
#include <vector>

const int MOD = 1000000007;
const int MAXN = 1000000;

long long fact[MAXN + 5];
long long invFact[MAXN + 5];
long long D[MAXN + 5];

// 快速幂
long long quick_power(long long base, long long exp) {
    long long res = 1;
    base %= MOD;
    while (exp > 0) {
        if (exp & 1) res = (res * base) % MOD;
        base = (base * base) % MOD;
        exp >>= 1;
    }
    return res;
}

// 预处理组合数学基础状态与错排状态
void init_structures(int n) {
    // 1. 预处理组合数
    fact[0] = 1;
    for (int i = 1; i <= n; ++i) fact[i] = (fact[i - 1] * i) % MOD;

    invFact[n] = quick_power(fact[n], MOD - 2);
    for (int i = n; i >= 1; --i) invFact[i - 1] = (invFact[i] * i) % MOD;

    // 2. 预处理错位排列状态
    D[1] = 0;
    D[2] = 1;
    for (int i = 3; i <= n; ++i) {
        // 严格带模加法,防止括号内相加后爆发越界
        D[i] = (i - 1) * ((D[i - 1] + D[i - 2]) % MOD) % MOD;
    }
}

// O(1) 获取组合数
long long C(int n, int m) {
    if (m < 0 || m > n) return 0;
    return fact[n] * invFact[m] % MOD * invFact[n - m] % MOD;
}

// O(1) 解决盒子可空的放球模型:n个同球入m个异盒
long long balls_in_boxes(int n, int m) {
    return C(n + m - 1, m - 1);
}

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

    init_structures(MAXN);

    int t;
    if (std::cin >> t) {
        while (t--) {
            int n, m;
            std::cin >> n >> m;
            // 示例:输出从n个元素选m个的组合数,以及n个元素的错排数
            std::cout << C(n, m) << " " << D[n] << "\n";
        }
    }
    return 0;
}

NOIP 实战避坑指南

  1. 错位排列乘法分配律漏取模导致数据越界
    在执行 D[i] = (i - 1) * (D[i - 1] + D[i - 2]) 时,内部 D[i - 1] + D[i - 2] 的最大值可能接近 $2 \times 10^9$。如果不加括号及时取模,再乘上外侧的 (i - 1)(若 $i \approx 10^6$),中间结果会达到 $2 \times 10^{15}$,直接把 int 撑爆引发符号位反转。必须严格保证每一步加法与乘法后都有 % MOD
  2. 放球模型变式中混淆“球同”与“球异”
    隔板法仅适用于球相同、盒子不同的情况。如果题目描述为“$n$ 个不同的球放入 $m$ 个不同的盒子”,部分选手还会惯性套用 $C_{n+m-1}^{m-1}$,这会导致逻辑全盘崩溃(异球入异盒本质上是第二类斯特林数或通道计数模型 $m^n$)。审题时必须敏锐捕捉“相同”与“不同”的限定词。

经典 NOIP/洛谷 真题

1. 洛谷 P4071 [SDOI2016] 排列计数

2. 洛谷 P3182 [HAOI2016] 放戏

原文链接: local://20.2

[h] 返回首页