← 目录 第 38 / 60 节 全部 60 节

38 · 组合计数与容斥原理

把“数方案”拆成乘法与加法两个动作,用杨辉三角把组合数算到模数下,再用容斥把“至少一个”化成“总数 − 一个都没有”,顺手拿下错排。
提高-预计 70 分钟组合数容斥卢卡斯
本节出现的新函数 / 新语法(先看懂再用)

计数题只做两件事。乘法原理:一件事分几步做完,各步的选择互不影响,总方案数是各步之积。加法原理:把全部方案切成互不重叠的几类,总方案数是各类之和。排列、组合、容斥、错排,全是这两条的推论。

先问“分几步”,再问“分几类”,最后问“取模取在哪一步”。三个问题答完,代码就定型了。

从 n 个不同元素里取 m 个排成一排:第一个位置有 n 种选法,第二个 n−1 种,……,第 m 个 n−m+1 种。乘法原理直接给出 A(n,m) = n(n−1)…(n−m+1) = n!/(n−m)!。如果不关心顺序,同一组 m 个元素被数了 m! 次(它们内部的 m! 种排法都算同一种选法),于是 C(n,m) = A(n,m)/m! = n!/(m!(n−m)!)。

“先按有序数、再除掉重复计数”这个动作会反复出现:圆排列除以 n(每种圈法被它的 n 个起点各数了一次),含重复元素的排列除以各元素个数的阶乘。记住这句,比背五个公式有用。

名称公式什么时候用小例子
排列 A(n,m)n!/(n−m)!选出 m 个还要排顺序(座次、名次)5 人排 3 个位置 = 60
组合 C(n,m)n!/(m!(n−m)!)只选出 m 个,不管顺序5 人选 3 人 = 10
可重排列nᵐ每个位置独立有 n 种选法4 位数字密码 = 10000
可重组合(隔板)C(n+m−1, m)m 个相同物品分给 n 人,允许空手3 个球分给 2 人 = 4
圆排列(n−1)!围成一圈,旋转后相同算一种5 人围桌 = 24

第三个动作是取模,它和公式对错完全无关。看这组数:

数值十进制位数存得下吗
C(30,15) = 1551175209 位int 可以
C(34,17) = 233360622010 位int 不行,long long 可以
C(62,31) = 46542835325526108818 位long long 刚好
C(100,50) ≈ 1.01×10²⁹30 位long long 不行
C(2000,1000)约 600 位必须取模

2000! 有 5736 位。所以“这题要不要取模”在读完数据范围的那一刻就定了,不存在“先算出来再取模”这种选项。本节只讲两套组合数求法中最基础的一套,它只有一个加法,因此对模数没有任何要求。

新函数拆解 · 组合数递推表 C[i][j](杨辉三角)
C[i][0] = C[i][i] = 1 % MOD; C[i][j] = (C[i-1][j-1] + C[i-1][j]) % MOD; // 1 <= j <= i-1
组成含义 / 格式要求
C[i][j]从 i 个不同元素里选 j 个的方案数,表里存的是 mod MOD 之后的值。i 是行,j 是列。
两个边界C[i][0] = 1(一个都不选只有一种方案),C[i][i] = 1(全选)。这两格不能靠递推得到,必须手写。写成 1 % MOD 是为了让 MOD = 1 时也对。
递推式的来源盯住第 i 个元素:选它,则还要从剩下 i−1 个里选 j−1 个;不选它,则从 i−1 个里选 j 个。两类互不重叠,加法原理。这就是帕斯卡恒等式 C(i,j) = C(i−1,j−1) + C(i−1,j)。
j 的范围内层只跑 1 到 i−1。j = 0 和 j = i 由边界给出;j > i 的格子保持 0,语义上是“非法状态”,不能当成“C = 0,所以是 MOD 的倍数”。
取模写在哪只写在赋值那一句末尾,括号要包住整个加法。写成 C[i-1][j-1] + C[i-1][j] % MOD 是错的:只对第二项取了模,第一项还是原值。
MOD 不要求是质数整个式子只有加法,没有除法。k = 12、k = 100003 都能直接用。这也是本题能用它的原因:题目给的 k 由输入决定,不保证是质数。
复杂度时间 O(N²),空间 O(N²)。N = 2000 时约 2×10⁶ 次加法(半个三角),int 表 16 MB。
最小可运行示例
示例
const int NMAX = 2005;
int C[NMAX][NMAX];                  // C[i][j] = C(i,j) % MOD

void build(int n) {                 // 预处理第 0..n 行
    for (int i = 0; i <= n; ++i) {
        C[i][0] = C[i][i] = 1 % MOD;        // 边界:选 0 个、选全部
        for (int j = 1; j < i; ++j)
            C[i][j] = (C[i-1][j-1] + C[i-1][j]) % MOD;
    }
}
// build(5) 之后
// 第 4 行:1 4 6 4 1
// 第 5 行:1 5 10 10 5 1
注意

为什么取模不破坏递推:C[i][j] mod MOD 只由两个已经取过模的值相加得到,加法和取模可交换。这是它能被用在“答案 mod k”类题目上的根本原因。

另一套写法是阶乘 + 逆元:O(N) 预处理,O(1) 查 C(n,m),但要求 MOD 是质数且 n < MOD。那套写法的除法要用逆元,第 39 节讲。

新函数拆解 · 子集枚举与 __builtin_popcount(容斥的实现手法)
for (int S = 1; S < (1 << m); ++S) { // 枚举 m 个集合的所有非空组合 int bits = __builtin_popcount(S); // S 里有几个集合 int sign = (bits & 1) ? +1 : -1; // 奇数个:加;偶数个:减 if (sign > 0) ans += ...; else ans -= ...; }
组成含义 / 格式要求
S 的含义一个 m 位二进制数。第 k 位为 1 表示“选上第 k 个集合”。S 从 1 开始,正好跳过空集。
1 << m2 的 m 次方,即子集总数。m 到 20 时是 10⁶ 量级,能枚举;m 到 10³ 就绝不能枚举子集。
__builtin_popcountGCC 内建函数,返回参数二进制里 1 的个数,编译成一条 popcnt 指令。参数是 int 时用它,long long 要用 __builtin_popcountll。
符号容斥公式里“k 个集合的交”前面是 (−1)^(k+1):k 奇数取加号,偶数取减号。所以看 bits 的奇偶。
为什么一一对应m 个集合的并集展开后恰好有 2^m − 1 项,与 2^m − 1 个非空子集一一对应。这是容斥能用二进制枚举实现的全部理由。
最小可运行示例
示例
// 求 1..n 中与 m 互质的数的个数:对 m 的质因子容斥
// k = m 的质因子个数,p[0..k-1] = 这些质因子,n = 上界
int cnt = 0;                                  // cnt = "不互质"的个数
for (int S = 1; S < (1 << k); ++S) {
    long long prod = 1; int bits = 0;
    for (int i = 0; i < k; ++i)
        if (S >> i & 1) { prod *= p[i]; ++bits; }  // p[] 是 m 的质因子
    cnt += (bits & 1) ? n / prod : -(n / prod);    // 奇数个质因子:加;偶数个:减
}
cout << n - cnt << "\n";                           // 补集:总数减“不互质”
注意

n / prod 是“1..n 中能被 prod 整除的数的个数”。把“集合大小”化成一次整除,是容斥题里最关键的观察——否则交集根本算不出来。

prod 必须开 long long:几个质因子相乘很容易超过 int(例如 2·3·5·7·11·13·17·19 = 9699690,再加一个 23 就到 2.2×10⁸)。

组合数的两套求法,别用错场合

递推(本节):O(N²) 时间、O(N²) 空间,只要加法,模数任意。适合 n ≤ 5000、或者模数不是质数。

阶乘 + 逆元(第 39 节):O(N) 预处理、O(1) 查询,要求模数是质数且 n < 模数。适合 n ≤ 10⁶。

卢卡斯定理:n、m 到 10¹⁸ 而模数是小质数(如 100003)时用 C(n,m) ≡ C(n%p, m%p)·C(n/p, m/p) (mod p),递归拆到 n < p 为止。本节不展开,见到“n、m 到 10¹⁸ 且模数很小”要先想到它。

例题 · 组合数问题洛谷 P2822普及/提高-去洛谷做这题 ↗

给定 k 与 t 组询问。对每组 (n, m),统计有多少对 (i, j) 满足 0 ≤ i ≤ n、0 ≤ j ≤ min(i, m) 且 k 整除 C(i, j)。

输入:第一行 t 和 k;接下来 t 行每行两个整数 n、m(0 ≤ n, m ≤ 2×10³,1 ≤ t ≤ 10⁴,k 在本题目测点中为 2~21 之间的整数)。

输出:t 行,每行一个整数,为对应询问的答案。

输入: 1 2 3 3 输出: 1
例题怎么想:把“整除判定”变成“表里的 0”

直接把每个 (i, j) 的真值算出来是不行的:2000! 有 5736 位。但题目只问 k 是否整除 C(i,j),也就是问 C(i,j) mod k 是否等于 0。同余关系对加法、乘法封闭,所以整张表都可以只存模值。

杨辉三角的递推式只有加法:C(i,j) = C(i−1,j−1) + C(i−1,j)。取模后递推依然成立,而且不需要 k 是质数——这一条决定了本题只能用这套写法,不能用阶乘除逆元。

于是第一步:O(n²) 把 C(i,j) mod k 的整张表算出来。n ≤ 2000,只有 4×10⁶ 个格子,很快。

第二步是回答 t ≤ 10⁴ 次询问。每次询问要数的是区域 i ≤ n, j ≤ min(i, m) 里有多少个 0。裸着数就是 O(n·m) 一次,乘 10⁴ 次必然超时。要 O(1) 回答,只剩下二维前缀和。

定义 ok[i][j] = 1 当且仅当 j ≤ i 且 C(i,j) mod k = 0,前缀和 pre[i][j] = ok[i][j] + pre[i−1][j] + pre[i][j−1] − pre[i−1][j−1]。

这里藏着本题唯一的思维难点:j > i 的格子要算成 0,不是算成 1。数学上 C(i,j) = 0(j > i 时),0 是任何数的倍数;但题目统计范围是 0 ≤ j ≤ min(i,m),这些格子根本不在范围内。如果它们被记成 1,答案会明显偏大。用 j <= i 把它挡在外面。

挡好之后,pre[n][m] 就恰好等于答案:因为所有 j > i 的格子 ok 都是 0,二维前缀和自动只累加了合法区域,连 min 都不用写。

最后两个实现细节:① 表的边长要按 max(n, m) 开(本题固定开到 2005),否则 pre[n][m] 那格根本没被算过,读到 0;② 边界写 1 % k,k = 1 时 1 mod 1 = 0,写成字面量 1 就违反了“表里存模值”的约定。

完整代码
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2005;
int C[MAXN][MAXN];        // C[i][j] = C(i,j) mod k
int pre[MAXN][MAXN];      // pre[i][j] = 左上角矩形内 ok 的和
int qn[10005], qm[10005];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t; long long k;                 // k 最大到 10⁹ 也可能,用 long long 读不吃亏
    cin >> t >> k;

    int maxn = 0;
    for (int i = 0; i < t; ++i) {
        cin >> qn[i] >> qm[i];
        maxn = max(maxn, max(qn[i], qm[i]));   // m 可能比 n 大,上界要按两者取
    }

    for (int i = 0; i <= maxn; ++i) {
        C[i][0] = C[i][i] = 1 % k;      // 边界。写成 1,k = 1 时就会错
        for (int j = 1; j < i; ++j)
            C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % k;   // 只有加法,k 不必是质数
    }

    for (int i = 0; i <= maxn; ++i)
        for (int j = 0; j <= maxn; ++j) {
            // j > i 时 C(i,j) 名义上是 0,但不在统计范围内,必须记 0 而不是 1
            int ok = (j <= i && C[i][j] == 0) ? 1 : 0;
            int v = ok;
            if (i) v += pre[i - 1][j];
            if (j) v += pre[i][j - 1];
            if (i && j) v -= pre[i - 1][j - 1];   // 容斥去重:左上角被加了两次
            pre[i][j] = v;
        }

    for (int i = 0; i < t; ++i)
        cout << pre[qn[i]][qm[i]] << "\n";   // j>i 的格子全是 0,所以不用再写 min
    return 0;
}
逐行拆解
代码片段含义与理由
const int MAXN = 2005;n, m ≤ 2000,多留 5 格防手滑。
int C[MAXN][MAXN];2005² × 4 B ≈ 16 MB。存的是 mod k 之后的值;真值有 600 位,存不下也不需要。
int pre[MAXN][MAXN];再 16 MB,合计 32 MB。前缀和最大到 4×10⁶,int 足够。
int t; long long k;k 的输入范围题目没写死,读 long long 是免费的保险。
maxn = max(maxn, max(qn[i], qm[i]));★ 只按 n 取上界时,m 更大的询问会读到从未算过的格子(值是 0),答案偏小;数组恰好按 n 开小时就是真越界。
C[i][0] = C[i][i] = 1 % k;边界必须手写,而且必须是“模过”的 1。k = 1 时它应为 0——本题 k ≥ 2,但这个习惯要一次养成。
for (int j = 1; j < i; ++j)只跑内部。j = 0 与 j = i 上一句已经给了,重复算会覆盖成同一个值,纯属浪费。
C[i][j] = (a + b) % k取模的括号包住整个加法。少一个括号只对第二项取模,答案会随 n 增大而越来越偏小。
int ok = (j <= i && C[i][j] == 0) ? 1 : 0;★ 本题最容易错的一句。C 表里 j > i 的位置从没写过、值是 0,所以少了 j ≤ i 它们就全被当成“k 的倍数”,样例直接错成 7。
if (i && j) v -= pre[i - 1][j - 1];二维前缀和的容斥:多算了两块,要减掉左上角一次。下标为 0 时跳过,避免访问 pre[-1][*]。
cout << pre[qn[i]][qm[i]];答案直接就是这个前缀和,不需要再减去什么——非法格子已经被 ok 定义排除了。

容斥解决的是“至少满足一个”。把“满足条件 i”记成集合 Aᵢ,则

容斥原理
|A₁ ∪ A₂ ∪ … ∪ A_m| =
    Σ|Aᵢ|  −  Σ|Aᵢ∩Aⱼ|  +  Σ|Aᵢ∩Aⱼ∩Aₖ|  −  …  +  (−1)^(m+1)|A₁∩…∩A_m|
    (第一个求和枚举 1 个集合,第二个枚举 2 个,共 2^m − 1 项)

它为什么对:设某个元素恰好属于 r 个集合(r ≥ 1)。右边把它数了 C(r,1) − C(r,2) + C(r,3) − … 次,而这个交错和等于 1 − (1−1)^r = 1。不属于任何集合的元素被数 0 次。所以每个该被数的元素恰好被数一次。证明用的是二项式定理,见本节数学补给。

例:1..1000 中不能被 2、3、5 整除的数有多少个?令 A₂、A₃、A₅ 分别是被 2、3、5 整除的数。

容斥的一次完整计算
1000 − (500 + 333 + 200) + (166 + 100 + 66) − 33 = 266
  ↑总数   ↑一个集合           ↑两个集合的交        ↑三个集合
          500=|A₂|  333=|A₃|  200=|A₅|
          166=|A₂∩A₃|(÷6)  100=|A₂∩A₅|(÷10)  66=|A₃∩A₅|(÷15)  33=|A₂∩A₃∩A₅|(÷30)

注意每一步都只用到 ⌊n/d⌋:“1..n 中能被 d 整除的数的个数”。容斥题能不能做,几乎全看交集的集合大小能不能 O(1) 算出来。

容斥的常见外壳把什么当作集合的并交集大小怎么算
1..n 中与 m 互质“能被 m 的某个质因子整除”⌊n/prod⌋,prod 是若干质因子之积
全装错(错排)“第 i 封信装对了”的并集,再用 n! 减去它(n−k)!:固定 k 封信,其余随便放
恰好 k 个位置满足“第 i 个位置满足”的并集,共 C(n,k) 种选法C(n,k)·D(n−k):先定下 k 个,其余错排

错排是容斥最经典的应用。n 封信全部装错:令 Aᵢ = “第 i 封信装进了第 i 个信封”,要的是 |A₁∪…∪Aₙ| 的补集。|Aᵢ| = (n−1)!,|Aᵢ∩Aⱼ| = (n−2)!,k 个集合的交 = (n−k)!,而 k 个集合有 C(n,k) 种选法,于是

错排的容斥式
D(n) = n! − C(n,1)(n−1)! + C(n,2)(n−2)! − … + (−1)ⁿ·C(n,n)·0!
     = n! · ( 1 − 1/1! + 1/2! − 1/3! + … + (−1)ⁿ/n! )

这个和式里都是除法,不好直接算。把它化简成只含加减乘的递推:

错排的递推式
D(1) = 0,  D(2) = 1
D(n) = (n − 1) · ( D(n−1) + D(n−2) )        // n ≥ 3

证明:第 n 封信一定不在第 n 个信封里,设它进了第 k 个信封(k 有 n−1 种选法)。此时看第 k 封信,只有两种可能:① 它进了第 n 个信封——两封信互换,剩下 n−2 封要错排,方案数 D(n−2);② 它没进第 n 个信封——把“第 n 个信封”改名成“第 k 个信封”,剩下的正好是 n−1 封信的错排,方案数 D(n−1)。两类互不重叠,相加后乘 n−1。

前几项:0, 1, 2, 9, 44, 265, 1854, 14833, 133496, 1334961。手算 D(3) = 2 可以验证:只有 (2,3,1) 和 (3,1,2) 两种。D(13) = 2290792932 已经超 int,D(20) = 895014631192902121 还在 long long 内,D(21) = 18795307255050944540 就超了。

例题 · 信封问题洛谷 P1595普及-去洛谷做这题 ↗

某人写了 n 封信,有 n 个信封,求所有信都装错信封的方案数。

输入:一个整数 n(1 ≤ n ≤ 20)。输出:一行一个整数,表示方案数。

输入: 3 输出: 2
例题怎么想:认出它是错排,然后选递推

题意就是错排 D(n):n 个元素的排列中,没有一个元素落在自己的位置上。n ≤ 20。

有两条路。第一条是容斥和式 D(n) = n!(1 − 1/1! + 1/2! − …),要算阶乘、要做除法,还容易溢出。第二条是递推 D(n) = (n−1)(D(n−1) + D(n−2)),只有加法和乘法。选第二条。

边界:D(1) = 0(1 封信只有“装对”这一种放法,全错是不可能的),D(2) = 1(两封互换)。这两个值必须手写,因为递推式要减到 n−2。

方向:D(n) 依赖 D(n−1)、D(n−2),下标都比 n 小,所以 n 从小到大。

类型:D(20) ≈ 8.95×10¹⁷,18 位,long long 刚好。中间量 (n−1)·(D(n−1)+D(n−2)) 最大也才 8.95×10¹⁷,不溢出。

复杂度 O(n),n = 20 时 18 次乘法。这种“答案本身就要 18 位”的题,long long 不是保险而是必需品。

完整代码
#include <bits/stdc++.h>
using namespace std;

long long D[25];        // D[n] = n 封信全部装错的方案数,n ≤ 20

int main() {
    int n;
    cin >> n;

    D[1] = 0;           // 1 封信只有"装对"一种放法,全错 0 种
    D[2] = 1;           // 2 封信只有互换这一种放法,且两封都错
    for (int i = 3; i <= n; ++i)
        D[i] = (long long)(i - 1) * (D[i - 1] + D[i - 2]);   // 第 i 封有 i-1 个"错"位置可选

    cout << D[n] << "\n";
    return 0;
}
逐行拆解
代码片段含义与理由
long long D[25];n ≤ 20,开 25 富余。全局数组默认全 0,但 D[1]、D[2] 仍然必须显式写,因为它们的值不是 0 和 0。
D[1] = 0;边界一。n = 1 时下面的循环一次都不进,答案直接取这一格。
D[2] = 1;边界二。递推式用到前两项,所以边界必须给两项——这是硬性对齐关系:用到几项就补几个边界。
for (int i = 3; i <= n; ++i)从小到大。D[i] 读 D[i−1]、D[i−2],都算好了。
(i - 1) * (D[i-1] + D[i-2])第 i 封信有 i−1 个“不能选”的信封可选,每种选法下方案数是两类之和。注意是乘不是加——乘法原理。
(long long)(i - 1)i 是 int,(i−1) 与 long long 相乘时本来就会自动提升。这个强转是写给读者看的:提醒这里会产出 18 位的数。
cout << D[n] << "\n";答案是 D[n] 这一格。n = 1、2 时循环不执行,靠的就是上面两个边界。
取模与溢出:4 个能直接复现的症状

WA,答案偏小,样例就对不上:% k 只写了最后一项,例如 C[i][j] = C[i-1][j-1] + C[i-1][j] % k;。这样每个格子存的是两个模值之和,可能落在 [0, 2k−2];恰好等于 k 的那些格子不会被判成“是 k 的倍数”。凡是 + 或 * 出现在取模左边,括号就要包住整个表达式。

WA,输出负数:容斥里做了减法,最后没 (x % MOD + MOD) % MOD。C++ 的 % 对负数返回负数,和数学上的同余不是一回事。

WA 或 RE,n 到十几就开始错:用 int 存 C(n,m) 的真值。C(34,17) = 2333606220 已经超 int,C(62,31) 是 18 位,C(100,50) 是 30 位。看到“组合数”先估位数。

答案对但 TLE:每次询问都重新数一遍区域,复杂度 O(t·n·m) = 10⁴×2000×2000 ≈ 4×10¹⁰。预处理 + 二维前缀和才能把询问压到 O(1)。

P2822 特有的三个坑

WA,答案偏大,样例就会错:把 j > i 的格子算进了“C = 0,是 k 的倍数”。C 表里 j > i 的位置从没被写过,值就是 0,C[i][j] == 0 恒成立。少了 j <= i 这个条件,样例 1 2 / 3 3 会输出 7,而正确答案是 1。修法:ok = (j <= i && C[i][j] == 0)。

WA(上界取错):maxn 只按 n 取,而某个询问的 m 比 n 大,pre[n][m] 就没被算过。本题数组开得够大,不会真的越界,但读到的是 0:输入 1 2 / 3 5 会输出 0,正确答案是 1。上界必须取 max(maxn, max(qn[i], qm[i]))。数组若恰好按 n 开,就是真越界 RE。

k = 1 时全错:边界写成 C[i][0] = 1,而表里该存的是 1 mod 1 = 0。此后每个格子的“是否为 0”判断全反:输入 1 1 / 3 3 时正确答案是 10(1 整除一切),你的程序会输出 3。写成 1 % k 一次解决。

自己动手复现

关掉页面,从空文件把 P2822 敲一遍并提交,样例 1 2 / 3 3 必须输出 1。

改一改 ①:把 ok 那句里的 j <= i && 删掉,只留 C[i][j] == 0。样例立刻从 1 变成 7;再试 1 2 加 5 1,答案从 2 变成 3。这两组对比把“非法格子”到底是什么,说得清清楚楚。

改一改 ②:把 maxn 那行改成只取 max(maxn, qn[i]),然后输入 1 2 加 3 5。正确答案是 1,你会得到 0——因为 pre[3][5] 根本没被算过。

改一改 ③:把 1 % k 改成 1,输入 1 1 加 3 3。正确答案是 10(1 整除一切,所有格子都算),你的程序会输出 3。

改一改 ④:把前缀和整段删掉,直接对每个询问双重循环数一遍。自己造 t = 10⁴、n = m = 2000 的数据,看它要跑几秒——这就是“能过与不能过”的差别。

数学补给 · 二项式定理:所有组合恒等式的源头

杨辉三角第 i 行就是 (1+x)^i 展开后的系数。把 (1+x)ⁿ 看成 n 个括号相乘,每个括号里挑 x 或挑 1,挑出 x 的那 k 个括号有 C(n,k) 种选法,于是

二项式定理(a=1, b=x)
(1+x)^n = C(n,0) + C(n,1)x + C(n,2)x² + … + C(n,n)xⁿ

一般形式是 (a+b)ⁿ = Σ C(n,k)·a^(n−k)·b^k。代不同的值,就能一次拿到一批恒等式:

代什么得到用途
x = 1C(n,0)+C(n,1)+…+C(n,n) = 2ⁿ子集总数;杨辉三角每行之和
x = −1C(n,0)−C(n,1)+C(n,2)−… = 0大小子集一样多;容斥的证明
a=1,b=x 后求导再代 x=1Σ k·C(n,k) = n·2^(n−1)加权和,下一节算期望会用到
n = 5 展开1 + 5x + 10x² + 10x³ + 5x⁴ + x⁵与杨辉三角第 5 行完全一致

容斥公式的正确性就是 x = −1 的那一行。设某个元素恰好属于 r 个集合(r ≥ 1),它被右边数了

每个元素被数几次
C(r,1) − C(r,2) + C(r,3) − … + (−1)^(r+1)·C(r,r)
= 1 − [ C(r,0) − C(r,1) + C(r,2) − … + (−1)^r·C(r,r) ]
= 1 − (1−1)^r = 1

中间那个中括号整个就是 (1−1)^r = 0(r ≥ 1)。所以并集的每个元素恰好被数一次,不在并集里的元素被数 0 次。容斥公式不是“记下来的”,是二项式定理的一个特例。

再看错排和 e 的关系。把容斥式两边除以 n!:

错排概率收敛到 1/e
D(n)/n! = 1 − 1/1! + 1/2! − 1/3! + … + (−1)^n/n!
n = 10:D(10)/10! = 1334961/3628800 = 0.3678794643
1/e                     = 0.3678794412      差 2.3×10⁻⁸

所以 n ≥ 10 以后,n 封信全装错的概率就稳定在 36.79%,与 n 几乎无关。这也是那道“n 封信随机装入 n 个信封,期望有多少封装对”答案是 1 的原因:每封信装对的概率是 1/n,n 封加起来恒等于 1(线性性,下一节细讲)。

最后一条工程结论:组合数的增长由 n 决定,而可用的类型只由位数决定。C(2000,1000) 约 10⁶⁰⁰,任何固定宽度的整数都装不下,取模是唯一出路;而 C(62,31) 恰好 18 位,是 long long 的极限——这两个数字值得记住,它们划出了“能直接算”与“必须取模”的分界。

举一反三 · 想清楚这些再往下走
  1. P2822 如果改成“统计 k 不整除 C(i,j) 的对数”,前缀和要改哪几行?能不能不新建数组就得到答案?
  2. k = 2 时,“C(n,m) 是奇数”有 O(1) 判据:(n & m) == m(卢卡斯定理 mod 2)。用它能不能把询问做到 O(1)?既然能,题目为什么还要你写 O(n²)?
  3. 错排的容斥式里有 1/k!,如果题目要求把 D(n) 对 10⁹+7 取模,这个除法要怎么处理?为什么递推式没有这个问题?
  4. “恰好 k 个人坐对”的方案数是 C(n,k)·D(n−k)。这个式子用的是哪两条原理?为什么不用容斥也能得到?
  5. 容斥的项数是 2^m。什么情况下 m 会到 10³ 以上?那时该换什么思路(提示:想想“只有很少几个集合会被选中”的场合)?
  6. 为什么 A(2000,3) 可以直接算(约 8×10⁹,long long 够),而 C(2000,1000) 必须取模?给出一个“能不取模就先不取模”的实际好处。
自测:能全部回答再进下一节
挑战题 · 越狱
洛谷 P3197普及/提高-去洛谷做这题 ↗

监狱有 n 个房间,每个房间关押一个犯人,共有 m 种宗教,每个犯人信仰其中一种。如果相邻两个房间的犯人信仰相同,就可能发生越狱。求有多少种状态可能发生越狱,答案对 100003 取模。

输入:一行两个整数 m 和 n,分别是宗教数与房间数(1 ≤ m ≤ 10⁸,1 ≤ n ≤ 10¹²)。

输出:一行一个整数,表示答案 mod 100003。

引导性提示(先只看这里)

直接数“至少有一对相邻相同”很麻烦:n−1 个位置都可能相同,而且这些事件互相重叠。

反过来数“一对相邻相同的都没有”。第 1 个房间有 m 种选法,第 2 个房间呢?第 3 个呢?

总数减去它。两个幂的指数最大到 10¹²,要用快速幂,别写循环。

想看思路了?点开完整思路

总状态数:每个房间独立选一种宗教,mⁿ 种。

无越狱的状态:第 1 个房间 m 种;此后每个房间都不能与左边那个相同,各 m−1 种。乘法原理得 m·(m−1)^(n−1)。

答案 = (mⁿ − m·(m−1)^(n−1)) mod 100003。这就是容斥里最简的一种:“至少一个”= 总数 − “一个都没有”。

n = 1 时公式给 m − m·(m−1)⁰ = 0,符合题意(只有一个房间,谈不上相邻)。

实现要点:m 可能 ≥ 100003,先把 m 对 MOD 取模再算幂;指数 n ≤ 10¹² 必须 long long;相减后可能为负,要 (x % MOD + MOD) % MOD。

复杂度 O(log n),每个幂约 40 次乘法。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 100003;

long long qpow(long long a, long long b) {   // a^b mod MOD
    long long res = 1 % MOD;
    a %= MOD;
    while (b > 0) {
        if (b & 1) res = res * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return res;
}

int main() {
    long long m, n;
    cin >> m >> n;              // 先读宗教数 m,再读房间数 n
    m %= MOD;                   // 底数先压进模数,避免中间乘积溢出

    long long total = qpow(m, n);                              // 全部状态 m^n
    long long bad   = m * qpow((m - 1 + MOD) % MOD, n - 1) % MOD;  // 一个相邻相同都没有
    cout << (total - bad + MOD) % MOD << "\n";                 // 减法后补 MOD 再取模
    return 0;
}
关键片段拆解
片段作用
cin >> m >> n;题目输入顺序是“宗教数 m 房间数 n”。写反了样例都过不去,是本题最常见的 0 分原因。
m %= MOD;m 最大 10⁸,先取模。取模之后 m−1 可能是 −1,所以下面要 (m−1+MOD)%MOD。
m * qpow((m - 1 + MOD) % MOD, n - 1)无越狱方案数 m·(m−1)^(n−1)。注意这里的 m 已经取过模,与 total 里的底数保持一致。
(total - bad + MOD) % MOD两个模值相减可能为负,加 MOD 保证落在 [0, MOD)。少了这一步,答案会变成负数。
long long nn ≤ 10¹²,int 装不下。qpow 的第二个参数和 b >>= 1 都依赖它是 long long。
这题最容易错的地方
  • 把 m 和 n 读反。题目输入的顺序是“宗教数 m、房间数 n”,不是按“房间数 n 个”的习惯先读 n。
  • 指数用 int:n ≤ 10¹²,读进 int 直接溢出,答案随机。
  • 忘了 +MOD:真值非负,但两个模值谁大谁小并不固定。m = 4、n = 10 时 total = 48546、bad = 78732,直接相减得 −30186,答案错。
  • 不先 m %= MOD:m ≤ 10⁸ 时 a·a = 10¹⁶ 还在 long long 内,看似没事,但好习惯是底数一律先取模。
延伸阅读 / 参考