适用对象: 已完成 GESP C++ 1–4 级、能独立完成 GESP 四级编程题、目标 CSP-J 复赛的同学 使用方式: 自学讲义。全套 16 课,每课按 1.5 小时设计——例题必须自己动手写一遍,不许只看 学完本课,你应该能:
使用说明:
| 符号 | 含义 |
|---|---|
| ⭐ | 基础题,热身 |
| ⭐⭐ | 实战题,对应复赛 T1 难度 |
| ⭐⭐⭐ | 冲刺题,对应复赛 T2 难度 |
| ⚠️ | 考场易错点 |
| 📝 | 规则/结论速记 |
| 项目 | 内容 |
|---|---|
| 题量 | 4 道编程题(T1–T4) |
| 时长 | 3.5 小时 |
| 每题分值 | 100 分,总分 400 |
| 难度梯度 | T1 入门(语法+简单枚举/模拟)→ T2 基础算法 → T3 进阶 → T4 较难 |
| 评测方式 | 你看不到评测结果,考完后用测试点逐个打分 |
📝 每道题不是"对/错"两种结果。 一道题通常有 10~25 个测试点(或分成若干子任务),过一个测试点得一份分。T4 拿 20 分和 T4 拿 0 分,差距可能就是一个"暴力"程序。
数据范围通常长这样:
对于 40% 的数据,n ≤ 1000; 对于 100% 的数据,n ≤ 1000000。
这句话的意思是:就算你只会 O(n²) 的笨办法,把它写对,就稳拿 40 分。
考场铁律(按顺序执行):
评测机 1 秒大约能执行 10⁸(一亿)次简单运算。拿到题先看数据范围,倒推你能用什么复杂度:
| 数据范围 n | 能接受的复杂度 | 典型写法 |
|---|---|---|
| n ≤ 20 | O(2ⁿ) | 枚举所有子集 |
| n ≤ 500 | O(n³) | 三重循环 |
| n ≤ 5000 | O(n²) | 双重循环 |
| n ≤ 10⁶ | O(n) ~ O(n log n) | 单重循环、排序 |
| n ≤ 10⁹ | O(√n)、O(log n) 或 O(1) | 数学、枚举到 √n |
⚠️ n ≤ 10⁹ 时,for (int i = 1; i <= n; i++)
直接超时。看到 10⁹ 就要警觉:这题不让你逐个枚举。
CSP-J 复赛和平时刷题网站最大的区别:程序从文件读入,向文件输出,不是键盘和屏幕。
假设题目名是 apple,考场要求:
apple,代码保存为
apple/apple.cppapple.in 读入,输出写到
apple.out#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("apple.in", "r", stdin);
freopen("apple.out", "w", stdout);
// 下面正常写 cin / cout,不用改任何东西
int n;
cin >> n;
cout << n * 2 << endl;
return 0;
}freopen 的作用是把 cin 的来源改成
apple.in、把 cout 的去向改成
apple.out。加了这两行之后,其余代码和你平时写的一模一样。
本课程统一用 Dev-C++ 5.11 写代码(新建源代码 Ctrl+N,保存 Ctrl+S,编译运行 F11)。带 freopen 的程序这样测试:
apple/apple.cpp(先建好题目文件夹)apple.in(注意别变成
apple.in.txt,需要在资源管理器里打开"显示文件扩展名"),写入测试数据并保存apple.out,核对答案⚠️ 运行后黑窗口空白 ≠ 程序坏了。用了 freopen,答案就不在屏幕上,在
.out 文件里。
⚠️ 爆零清单(每年都有人踩,考前最后 30 分钟逐条检查):
apple,你写成
Apple.in、aple.in——0 分cout << "debug" << endl;)没删——答案错system("pause") 或等待输入——超时📝 从今天起,每道练习题都按考场格式写 freopen(做网站题时再注释掉),把肌肉记忆练出来。
枚举和模拟是 T1 的绝对主力:不需要高级算法,需要的是"读懂题 + 写对代码 + 算清复杂度"。
输入两个正整数 a 和 b(1 ≤ a, b ≤ 10⁹),求 aᵇ 的值。如果 aᵇ > 10⁹,输出
-1。输入样例1: 输入样例2: 10 9 2 31 输出样例1: 输出样例2: 1000000000 -1
第一步:先想暴力。 循环 b 次,每次乘一个 a:
ans = 1
重复 b 次:ans = ans * a
第二步:算复杂度预算。 b 最大 10⁹,循环 10⁹ 次——超时了?
先别慌,再想一层:只要 a ≥ 2,乘 31 次就超过 10⁹
了(2³¹ ≈ 2.1 × 10⁹)。所以循环里一旦发现
ans > 10⁹ 就立刻输出 -1
结束,循环实际最多跑 30 多次。
第三步:找漏网之鱼。 什么情况下 ans 永远不会超过 10⁹、循环会傻跑 b 次?——a = 1。1 乘多少次都是 1,b = 10⁹ 时循环真的会跑 10⁹ 次,超时。所以 a = 1 要特判。
完整代码:
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("pow.in", "r", stdin);
freopen("pow.out", "w", stdout);
long long a, b;
cin >> a >> b;
if (a == 1) { // 特判:不特判就会超时
cout << 1 << endl;
return 0;
}
long long ans = 1;
for (long long i = 1; i <= b; i++) {
ans = ans * a;
if (ans > 1000000000) { // 超过 10^9 立刻收工
cout << -1 << endl;
return 0;
}
}
cout << ans << endl;
return 0;
}⚠️ 两个易错点:
ans 必须用 long long。10⁹ × 10⁹ 会把
int 撑爆(int 上限约 2.1 ×
10⁹),乘完再判断就晚了a == 1 忘了写,能过大部分测试点,但 a = 1、b = 10⁹
的那个测试点会超时——这就是"100 分和 90 分的区别在细节"📝 T1 解题三步:想暴力 → 对着数据范围算复杂度 → 找特殊情况。
桌上有 n 个苹果排成一排(1 ≤ n ≤ 10⁹)。每天从第 1 个开始,每隔 2 个取走 1 个(即取走当天的第 1、4、7、… 个),剩下的苹果保持顺序重新排好,第二天继续。 求:(1) 取完所有苹果需要多少天;(2) 最初的第 n 个苹果(最后一个)在第几天被取走。
输入样例: 8 输出样例: 5 5
第一步:拿样例手玩一遍。 n = 8,每天取位置 1, 4, 7, …:
| 天 | 当天苹果数 m | 取走 | 剩下 |
|---|---|---|---|
| 1 | 8 | 3 个(位置1、4、7) | 5 |
| 2 | 5 | 2 个(位置1、4) | 3 |
| 3 | 3 | 1 个(位置1) | 2 |
| 4 | 2 | 1 个(位置1) | 1 |
| 5 | 1 | 1 个(位置1) | 0 |
5 天取完 ✓。手玩发现两个规律:
(m + 2) / 3)第二步:算复杂度。 不能一个个苹果模拟(n 到 10⁹),但可以按天模拟苹果总数:每天 m 变成 m − ⌈m/3⌉,约缩小到 2/3,10⁹ 只需要约 50 天。O(log n),随便过。
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("apple.in", "r", stdin);
freopen("apple.out", "w", stdout);
long long n;
cin >> n;
long long m = n, days = 0, lastDay = 0;
while (m > 0) {
days++;
if (m % 3 == 1 && lastDay == 0) {
lastDay = days; // 最后一个苹果在今天被取走
}
m = m - (m + 2) / 3; // 取走 ⌈m/3⌉ 个
}
cout << days << " " << lastDay << endl;
return 0;
}📝 模拟题的降维套路:不模拟每个"个体",模拟"数量"。 把 O(n) 的模拟压成 O(log n) 或 O(天数)。
📝 手玩样例不是浪费时间。 上面两个规律都是列表格玩出来的,不动手很难凭空想到。
每题按考场格式建文件夹、写 freopen(文件名用题目名)。
⭐ 热身 1:默写模板(题目名 t1)
合上讲义,默写带 freopen 的完整模板,编译通过为准。
⭐ 热身 2:3 或 5 的倍数(题目名
mul)
输入 n(1 ≤ n ≤ 10⁶),输出 1~n 中能被 3 或 5 整除的数的个数与总和,空格分隔。
输入样例:
10
输出样例:
5 33
1~10 中符合条件的是 3, 5, 6, 9, 10,共 5 个,和为 33。n ≤ 10⁶,O(n) 枚举随便过。
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("mul.in", "r", stdin);
freopen("mul.out", "w", stdout);
int n;
cin >> n;
long long cnt = 0, sum = 0;
for (int i = 1; i <= n; i++) {
if (i % 3 == 0 || i % 5 == 0) {
cnt++;
sum += i;
}
}
cout << cnt << " " << sum << endl;
return 0;
}sum 用 long long:最坏约 10⁶ × 10⁶ / 2
量级?不对——和的上界约为 10⁶ 个数、每个不超过 10⁶,总和上界约 5 ×
10¹¹,超出 int,必须 long long。
⭐⭐ 实战:数字统计(NOIP 2010 普及组 T1,题目名
two)
输入两个整数 L 和 R(1 ≤ L ≤ R ≤ 10⁵),统计 L~R 的所有整数中,数字
2 一共出现了多少次。例如 22 里有两个 2。
输入样例:
2 22
输出样例:
6
2, 12, 20, 21 各贡献 1 个,22 贡献 2 个,共 6。外层枚举每个数,内层逐位拆数字:
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("two.in", "r", stdin);
freopen("two.out", "w", stdout);
int l, r;
cin >> l >> r;
int cnt = 0;
for (int i = l; i <= r; i++) {
int x = i;
while (x > 0) {
if (x % 10 == 2) cnt++; // 看个位
x = x / 10; // 砍掉个位
}
}
cout << cnt << endl;
return 0;
}复杂度 O((R−L) × 位数),10⁵ × 6 = 6 × 10⁵,远低于预算。
📝 逐位拆数字的 while (x > 0) { x % 10; x /= 10; }
是枚举题高频零件,背下来。
⭐⭐⭐ 冲刺:约数个数(题目名 div)
输入 n(1 ≤ n ≤ 10¹²),输出 n 的约数(因数)个数。
先想:从 1 枚举到 n 是什么复杂度?过得了吗?过不了怎么办?(提示:约数总是成对出现的——如果 i 整除 n,那么 n/i 也整除 n。)
输入样例:
12
输出样例:
6
12 的约数:1, 2, 3, 4, 6, 12,共 6 个。
枚举到 n 是 O(n) = 10¹² 次,超时。利用"约数成对出现":只枚举 i ≤ √n,找到一个 i 就同时数上配对的 n/i,复杂度降到 O(√n) = 10⁶ 次。
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("div.in", "r", stdin);
freopen("div.out", "w", stdout);
long long n;
cin >> n;
long long cnt = 0;
for (long long i = 1; i * i <= n; i++) {
if (n % i == 0) {
cnt += 2; // i 和 n/i 配一对
if (i * i == n) cnt--; // 完全平方数:i 和 n/i 是同一个,别数两次
}
}
cout << cnt << endl;
return 0;
}⚠️ 循环条件写 i * i <= n 而不是
i <= sqrt(n):浮点开方有精度误差,整数乘法没有。i
用 long long,否则 i * i 在 10¹² 附近会把
int 撑爆。
📝 "枚举到 √n" 是把 10¹² 级别数据拉回预算内的第一个标准武器。
考场模板(默写级):
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("题目名.in", "r", stdin);
freopen("题目名.out", "w", stdout);
return 0;
}复杂度预算表:
| n 的量级 | 复杂度上限 |
|---|---|
| 20 | O(2ⁿ) |
| 500 | O(n³) |
| 5000 | O(n²) |
| 10⁶ | O(n log n) |
| 10⁹ 以上 | O(√n) / O(log n) / O(1) |
爆零清单: 文件名错、freopen
注释没恢复、文件夹没建对、调试输出没删、system("pause")
没删。
T1 解题三步: 想暴力 → 对数据范围算复杂度 → 找特殊情况(a=1、n=0、完全平方数……)。
本课新零件:
freopen、(m + 2) / 3
向上取整、while (x > 0) { x % 10; x /= 10; }
拆位、i * i <= n 枚举到根号。