用 Pascal 递推预处理组合数对 k 的余数,再对可整除位置建立二维前缀和。
OJ: luogu
题目 ID: P2822
难度:普及/提高-
标签:组合计数动态规划前缀和
日期: 2026-06-22 23:14
题意
给定固定的
其中
思路
朴素做法的瓶颈
小数据可以先递推组合数对
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-19 11:59
* update_at: 2026-07-19 11:59
*/
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:用 Pascal 递推直接算小范围组合数取模,再枚举询问范围。
const int MAXN = 105;
int t, k;
int comb_mod[MAXN][MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> t >> k;
comb_mod[0][0] = 1 % k;
for (int i = 1; i < MAXN; i++) {
comb_mod[i][0] = comb_mod[i][i] = 1 % k;
for (int j = 1; j < i; j++) {
comb_mod[i][j] = (comb_mod[i - 1][j - 1] + comb_mod[i - 1][j]) % k;
}
}
while (t--) {
int n, m;
cin >> n >> m;
int answer = 0;
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= min(i, m); j++) {
if (comb_mod[i][j] == 0) {
answer++;
}
}
}
cout << answer << '\n';
}
return 0;
}一组询问最多检查约
Pascal 递推只保留余数
组合数满足:
题目只关心组合数能否被
从第 combination[j] 和 combination[j-1] 时,它们仍然都是上一行的值,不会被当前行提前覆盖。
样例 DP 表
这张表展示样例
i \ j |
0 |
1 |
2 |
3 |
|---|---|---|---|---|
0 |
1 |
- | - | - |
1 |
1 |
1 |
- | - |
2 |
1 |
0 |
1 |
- |
3 |
1 |
1 |
1 |
1 |
行表示 0 的位置就是可被
对可整除位置建立二维前缀和
定义:
再建立二维前缀和:
询问 sum[n][min(n,m)]。虽然矩形中还包含 bad 被定义为 0,不会造成误计。
原 main.py 已使用相同模型,但预处理仍包含约四百万次 Python 层二维前缀更新和多次对象访问,在评测限制下没有通过。C++ 使用连续的全局整型数组,并把 Pascal 余数压成一行,预处理开销和内存都更稳定。
正确性说明
Pascal 恒等式对所有合法 combination[j] 正确保存当前
于是 bad[i][j]=1 当且仅当
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-19 11:59
* update_at: 2026-07-19 11:59
*/
#include <bits/stdc++.h>
using namespace std;
const int LIMIT = 2000;
int test_count, divisor;
int combination[LIMIT + 1]; // 当前 Pascal 行的 C(i,j) mod k。
int prefix_bad[LIMIT + 1][LIMIT + 1];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> test_count >> divisor;
combination[0] = 1 % divisor;
for (int i = 0; i <= LIMIT; i++) {
if (i > 0) {
// 从右向左更新,避免覆盖本轮仍要使用的上一行状态。
for (int j = i; j >= 1; j--) {
combination[j] = (combination[j] + combination[j - 1]) % divisor;
}
}
for (int j = 0; j <= LIMIT; j++) {
int divisible = (j <= i && combination[j] == 0 ? 1 : 0);
int up = (i > 0 ? prefix_bad[i - 1][j] : 0);
int left = (j > 0 ? prefix_bad[i][j - 1] : 0);
int diagonal = (i > 0 && j > 0 ? prefix_bad[i - 1][j - 1] : 0);
prefix_bad[i][j] = up + left - diagonal + divisible;
}
}
while (test_count--) {
int n, m;
cin >> n >> m;
if (m > n) m = n;
cout << prefix_bad[n][m] << '\n';
}
return 0;
}复杂度
令上界
- Pascal 递推和二维前缀和预处理时间为
。 - 每组询问只访问一个前缀和位置,时间为
;总时间复杂度为 。 - 二维前缀和占用
空间,滚动的组合数余数行占用 空间。 brute.cpp每组询问直接枚举,最坏为,只用于小数据对拍。
总结
大量询问共享同一个