先用杨辉三角取模标记 C(i,j) 是否被 k 整除,再用二维前缀和 O(1) 回答。
P2822 [NOIP 2016 提高组] 组合数问题
GESP8 冲刺课堂页:先建模,再压缩状态,最后读代码。
一、先回答问题
每个询问重新枚举所有 (i,j),T 到 1e4 时会超时。
二、核心观察
mark[i][j]=1 表示 C(i,j)%k==0,s[i][j] 表示左上矩形里的标记总数。
为什么不丢答案:j>i 的位置记 0,查询区域就能统一变成矩形前缀。
C(i,j)%k
mark
前缀和 s
三、互动演示
步骤1
关键状态 A-
关键状态 B-
答案/目标-
复杂度看表
课堂动作手推
点击下一步开始。
四、自己选答案
为什么只存取模结果?
先判断,再点按钮。
m>n 怎么办?
先判断,再点按钮。
五、手推结果
| 阶段 | 状态 | 作用 |
|---|---|---|
| 组合数 | c[i][j] | 杨辉递推取模 |
| 标记 | mark | 是否整除 |
| 查询 | s | 矩形计数 |
课堂要求:每题先说清对象、关系、保留的信息、压缩掉的信息,再写代码。
六、C++14 参考代码
#include <bits/stdc++.h>
using namespace std;
int c[2005][2005], s[2005][2005];
int t, k;
void precompute() {
// 杨辉三角递推组合数并取模
c[0][0] = 1 % k;
for (int i = 1; i <= 2000; i++) {
c[i][0] = 1 % k;
for (int j = 1; j <= i; j++) {
c[i][j] = (c[i-1][j] + c[i-1][j-1]) % k;
}
}
// 计算二维前缀和
for (int i = 1; i <= 2000; i++) {
for (int j = 1; j <= 2000; j++) {
int val = (j <= i && c[i][j] == 0) ? 1 : 0;
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + val;
}
// 对于这一行的超出 i 的部分,直接继承上一列的值
// 但其实由于 val 在 j > i 时为 0,上面的公式也适用
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> t >> k;
precompute();
while (t--) {
int n, m;
cin >> n >> m;
// 组合数 C(i, j) 中 j 不能大于 i
if (m > n) m = n;
cout << s[n][m] << "\n";
}
return 0;
}