计数题只做两件事。乘法原理:一件事分几步做完,各步的选择互不影响,总方案数是各步之积。加法原理:把全部方案切成互不重叠的几类,总方案数是各类之和。排列、组合、容斥、错排,全是这两条的推论。
从 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) = 155117520 | 9 位 | int 可以 |
| C(34,17) = 2333606220 | 10 位 | int 不行,long long 可以 |
| C(62,31) = 465428353255261088 | 18 位 | long long 刚好 |
| C(100,50) ≈ 1.01×10²⁹ | 30 位 | long long 不行 |
| C(2000,1000) | 约 600 位 | 必须取模 |
2000! 有 5736 位。所以“这题要不要取模”在读完数据范围的那一刻就定了,不存在“先算出来再取模”这种选项。本节只讲两套组合数求法中最基础的一套,它只有一个加法,因此对模数没有任何要求。
| 组成 | 含义 / 格式要求 |
|---|---|
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 节讲。
| 组成 | 含义 / 格式要求 |
|---|---|
S 的含义 | 一个 m 位二进制数。第 k 位为 1 表示“选上第 k 个集合”。S 从 1 开始,正好跳过空集。 |
1 << m | 2 的 m 次方,即子集总数。m 到 20 时是 10⁶ 量级,能枚举;m 到 10³ 就绝不能枚举子集。 |
__builtin_popcount | GCC 内建函数,返回参数二进制里 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¹⁸ 且模数很小”要先想到它。
给定 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 行,每行一个整数,为对应询问的答案。
直接把每个 (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 就超了。
某人写了 n 封信,有 n 个信封,求所有信都装错信封的方案数。
输入:一个整数 n(1 ≤ n ≤ 20)。输出:一行一个整数,表示方案数。
题意就是错排 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 时循环不执行,靠的就是上面两个边界。 |
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)。
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) 种选法,于是
(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 = 1 | C(n,0)+C(n,1)+…+C(n,n) = 2ⁿ | 子集总数;杨辉三角每行之和 |
| x = −1 | C(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!:
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 的极限——这两个数字值得记住,它们划出了“能直接算”与“必须取模”的分界。
(n & m) == m(卢卡斯定理 mod 2)。用它能不能把询问做到 O(1)?既然能,题目为什么还要你写 O(n²)?+MOD 再 %MOD,并说得出一个会踩到这个坑的场景。监狱有 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 n | n ≤ 10¹²,int 装不下。qpow 的第二个参数和 b >>= 1 都依赖它是 long long。 |
+MOD:真值非负,但两个模值谁大谁小并不固定。m = 4、n = 10 时 total = 48546、bad = 78732,直接相减得 −30186,答案错。m %= MOD:m ≤ 10⁸ 时 a·a = 10¹⁶ 还在 long long 内,看似没事,但好习惯是底数一律先取模。