刷 OJ 到第五天,13 到 15 题正好卡在一个门槛上。前几天你还在练“输入两个整数求 a+b”这种热身题,到了这个阶段,题目开始真正考察你写程序的稳定性:逻辑要一次想对,边界条件要想全,提交后面对红色状态要能冷静地定位问题。我先把结论放在这里:第五天做的 13 到 15 题,大概率不是送分题,而是把循环、数组、整型操作以及一点点数学思维揉在一起的综合题。
如果你和我一样,白天有课或有工作,只能抽晚上刷题,那第五天是特别容易放弃的一段。前四天的新鲜感过去了,题目难度开始爬坡,网上搜题解的冲动也最强。我的建议是:13 到 15 题不要贪快,哪怕每天彻底搞懂一道,也比一口气吞下五道有用得多。这篇文章就围绕“第五天 OJ 13 到 15 题”这个阶段,把题型、解法、提交坑位和刷题节奏一次说透。
1. 第五天刷题,13到15题到底在考什么
1.1 从题目序号看题单的递进规律
绝大多数在线评测平台在排新手题单的时候,都会遵循一条看不见的“难度曲线”。刚开始的几道题只考你会不会读入、会不会输出,比如经典的 A+B;接下来几道题开始考分支判断,比如判断闰年、比较大小;再到 10 道题往后,题单会悄悄把循环、数组、多组输入这些内容组合起来考。我刷过的杭电 OJ、郑州轻工业 OJ、东方博宜 OJ 等平台,虽然题目编号不完全一样,但规律基本一致:13 到 15 题正好落在“我能写出代码”向“我能一次写对代码”过渡的区间。
这个过渡不是平台故意刁难人,而是必要的筛选机制。前面的题目像跑步热身,让你熟悉 OJ 的输入输出格式、提交按钮、评测状态这些环境。热身结束后,平台默认你已经知道 CE 是编译错误、WA 是答案错误、TLE 是超时,所以题目不再手把手喂你,开始要求你自己处理“题目没明说但客观上存在”的情况,比如多组输入到底读到哪结束,比如结果要保留几位小数,比如最大值初始化该用多少。这些细节单独拎出来都不难,但凑在一起,就成了第一次劝退点。
1.2 13到15题最常考察的三种能力
第一种是多组输入与循环控制。很多第一次碰到“输入包含多组测试数据,处理到文件结束”的同学会懵,不知道用while (cin >> n)而不是cin >> n。这一阶段反复出现这种读入方式,就是在逼你养成“读入跟着题目描述走,而不是跟着自己的直觉走”的习惯。
第二种是边界条件意识。0、1、最大值、最小值、负数、前导零,这些词听上去很抽象,但 WA 的提交里十有八九挂在它们身上。比如“把一个整数倒序输出,去掉前导零”这道题,输入 120 时输出应该是 21 而不是 021;输入 0 时输出应该是 0 而不是空。这些细节只有你想到了,代码才会对。
第三种是自我调试能力。OJ 不会告诉你错在哪个输入上,你必须学会自己造数据、看输出、猜原因。有人搜“西北农林科技大学 C++ OJ 智能指针”,那是学到数据结构和现代 C++ 才会碰到的东西,13 到 15 题阶段完全用不到,不如先把边界条件练好。
2. 第13到15题的典型题型拆解
不同平台的第 13 到 15 题具体内容可能不同,但这阶段常见的题型高度集中。我把它们归成三类,你对着题单看,大概率能认出来。就算你遇到的题目跟我的描述不完全一样,拆题思路也通用:先读清楚输入是什么、输出要什么格式,再想数据范围,再写代码。
2.1 第13题:数列求和与精度格式化
这类题的典型描述是:输入一个整数 n,求 S = 1 + 1/2 + 1/3 + ... + 1/n 的值,结果保留三位小数。它同时考三件事:累加循环、整数除法和浮点数除法之间的区别、输出格式化。
第一件事不用多说,for循环从 1 加到 n 就完事。第二件事是新手最容易翻车的点:如果你写1 / i,C++ 会按整数除法处理,i 一旦大于 1,结果就是 0,最后累加出来 S 等于 1。要得到正确结果,必须写1.0 / i,让参与除法运算的两个数中至少有一个是浮点数。第三件事是格式化输出,用cout << fixed << setprecision(3)就能把输出固定为小数点后三位。
这类题看着简单,但能一次性 AC 的人并不多。很多人卡在精度上,比如题目要求四舍五入保留三位小数,如果你用单精度float而不是double,数据稍大一点累加误差就会被放大。能用 double 就用 double,不要跟浮点进度较劲。
2.2 第14题:数组最值查找
典型描述是:输入一个正整数 n,然后给出 n 个整数,输出其中的最大值和最小值。这类题考数组操作和比较逻辑,看起来直白,实际上隐藏着初始化陷阱。
新手最常见的写法是:
int maxv = 0; int minv = 0;如果 n 个整数全是负数,maxv就会一直是 0,答案错误;如果全是正数,minv又输出了 0。正确做法有两种:要么把最大值和最小值都初始化为第一个读入的元素,要么用<climits>头文件里的INT_MAX和INT_MIN。后一种思路更通用,先拿一个很大的数当作最小值候选,再拿一个很小的数当作最大值候选,每读一个数就比一次。
这道题的另一个考点是输入格式:先读 n,再循环读 n 个数。有些同学会把 n 当成第一个要比较的数,导致少读一个数据、拿随机数去比较,最终答案错得莫名其妙。我建议每道题先手动跑一遍样例,把变量每一步的值写在草稿纸上,这种错误基本上当场就能抓出来。
2.3 第15题:数字反转与符号处理
典型描述是:输入一个整数,把它的各位数字反转后输出,要求去掉前导零,负数保留负号。比如输入 -120,输出 -21;输入 0,输出 0。这道题考察整除、取模、负号处理,还顺带考察“前导零”概念,是这一阶段含金量比较高的题。
核心思路是循环“取最后一位,拼到新数字后面”。第一步先保存符号,把负数转成正数处理;第二步用while循环,每次取n % 10,把结果乘以 10 之后加上这一位;第三步把符号乘回去。关于前导零,实际上反转算法天然会处理掉:输入 120,先取出 0,rev变成 0,再取出 2,rev变成 2,再取出 1,rev变成 21,最后输出 21 而不是 021,因为整数类型本身不会保存前导零。
这道题还要留个心眼:如果题目数据范围允许到int的极限,比如 n 是 INT_MIN,那么n = -n会溢出,出现未定义行为。做题前先看题目给的范围,如果 n 的绝对值可能超过int能表示的范围,可以直接用long long来存,省心很多。
3. 完整解题实操展示
光讲题型还是有点虚,我把这三道题的完整代码和实测流程直接放出来。以下代码我在好几个平台上跑过,主要思路一致,个别平台的编译器版本不同,注意用 C++11 或更高版本就没问题。
3.1 三个题目的完整 C++ 代码
第 13 题,数列求和:
#include <iostream> #include <iomanip> using namespace std; int main() { int n; cin >> n; double sum = 0.0; for (int i = 1; i <= n; i++) { sum += 1.0 / i; } cout << fixed << setprecision(3) << sum << endl; return 0; }注意1.0 / i前面的 1.0 是关键,不能写成 1。fixed控制小数点后的位数,setprecision(3)表示保留三位小数。如果题目要求保留六位,就把 3 改成 6。
第 14 题,数组最值:
#include <iostream> #include <climits> using namespace std; int main() { int n; while (cin >> n) { if (n <= 0) continue; int maxv = INT_MIN; int minv = INT_MAX; for (int i = 0; i < n; i++) { int x; cin >> x; if (x > maxv) maxv = x; if (x < minv) minv = x; } cout << maxv << " " << minv << endl; } return 0; }这里用while (cin >> n)是因为很多 OJ 题目写的是“多组输入,处理到文件结束”。如果题目只要求处理一组数据,只写一次cin >> n也够,但写成 while 循环也没错,反而能应对额外测试数据。INT_MAX和INT_MIN的引入是这道题最稳妥的初始化方式。
第 15 题,数字反转:
#include <iostream> using namespace std; int main() { long long n; cin >> n; long long sign = 1; if (n < 0) { sign = -1; n = -n; } long long rev = 0; while (n > 0) { rev = rev * 10 + n % 10; n /= 10; } cout << sign * rev << endl; return 0; }用long long是为了避免INT_MIN取反溢出的问题,比较稳妥。输入 0 时,while 循环不会执行,rev保持 0,输出0,结果正确。
3.2 输入输出样例怎么自测
代码写完别急着提交,先在本地编译器上跑几个样例,确认输出格式跟题目要求完全一致。我以三组典型输入演示一遍:
第 13 题,输入:
3输出:
1.833因为 1 + 0.5 + 0.333 约等于 1.833,保留三位小数。
第 14 题,输入:
5 1 2 3 4 5输出:
5 1注意最大值和最小值之间是空格,末尾是换行,中间不能有多余的空格或逗号。
第 15 题,输入:
-120输出:
-21如果输出成-021或者21都是错的。整数类型不会输出前导零,所以正常反转逻辑就会自动满足“去掉前导零”的要求。
3.3 边界值验证清单
自测样例通过只是第一步,我每次提交前会过一遍边界清单:
- n 等于 1,比如数列求和只加一项;
- n 等于 0,如果题目允许,看看程序会不会崩;
- 全是负数的最值查找,确认 maxv 不会再输出 0;
- 带负号的数字反转,确认符号没有被丢掉;
- 结尾带 0 的数字,比如 120,确认输出不是 “021” 而是 21;
- 可能的最大值、最小值,确认没有溢出。
这组清单看着琐碎,但能挡住 80% 的 WA。很多同学代码逻辑大方向没问题,最后就是挂在“我没想到输入会是负数”这种地方。
4. OJ提交过程中的坑位速查
这一节是实操中最容易卡住的部分。我把自己刷题第五天遇到过的坑、以及在群里看到别人踩过的坑都整理出来,按提交后最常见的状态分类。
4.1 看懂评测状态:AC、WA、PE、TLE、RE、CE
OJ 提交后会返回一个简短的状态,很多人第一次看到错误状态就慌了。其实这些状态含义很固定:
| 状态 | 含义 | 常见原因 |
|---|---|---|
| AC | Accepted,答案正确 | 无 |
| WA | Wrong Answer,答案错误 | 逻辑、边界条件、精度问题 |
| PE | Presentation Error,格式错误 | 多空格、少换行、输出格式不完全吻合 |
| TLE | Time Limit Exceeded,超时 | 死循环、算法太慢 |
| RE | Runtime Error,运行时错误 | 数组越界、除零、空指针 |
| CE | Compile Error,编译错误 | 语法错误、缺头文件、编译器版本不兼容 |
其中 PE 最容易让人头大。明明数据对,但因为它要求每组输出之间多打一个空行,或者最后一个数字后面不能有空格,就被判不通过。处理办法是严格复制题面输出样例的格式,不要自己发挥。
4.2 编译错误与运行时错误的处理
编译错误最常见的原因是缺头文件。用了cout没写#include <iostream>,用了INT_MAX没写#include <climits>,用了setprecision没写#include <iomanip>,这些都属于低级但高频的错误。我的习惯是先把整道题要用的功能列出来,再决定包含哪些头文件。
运行时错误里,数组越界是最典型的。如果你开了int a[100],但题目说 n 最大是 1000,越界就是必然的。不要为了省空间把数组开得太小,不确定范围时开大一个量级不会有惩罚。
提示:本地编译器不报错不代表 OJ 上能过。本地运行遇到越界往往会“碰巧正常”,但 OJ 的评测环境可能严格得多,所以数组大小的设置一定要参考题目的数据范围。
4.3 答案错误与格式错误
WA 和 PE 是刷题第五天最常碰到的状态。WA 要考虑边界和精度;PE 要先检查输出格式。我见过一个同学第 14 题输出写成cout << maxv << '\t' << minv << endl;,用制表符代替空格,本地看是整齐,提交后一直 PE,改成空格立刻就 AC 了。
多个测试用例时的换行也是一个常考点。如果题目要求“每组输出占一行”,你就要保证每组之间没有多余空行;如果题目要求“每组输出后跟一个空行”,你还需要额外输出空行。判断方法很简单:看样例输出里两组数据之间有没有空行。样例里没有,你就不要加。
4.4 超时和溢出问题
13 到 15 题阶段遇到 TLE 的概率比较低,但不代表不会遇到。最常见的原因是while循环条件写错,导致死循环。比如判断输入是否结束时,你写的是while (cin >> n != 0),语义容易混乱;正确写法是while (cin >> n),读入失败循环自然结束。
溢出问题也要提前想。第 13 题如果 n 很大,double一般够用;但如果题目换成阶乘求和,int很快就爆了,要用long long,必要的时候甚至要用到高精度。做题前先估一下最坏情况下的数值大小,这个习惯应该从第 13 题就开始养。
5. 刷题节奏与复盘方法
5.1 错题本怎么写才有用
第五天刷题,错题本的价值开始体现。不要直接把别人的 AC 代码抄一遍贴上去,那样下次还是不会。我的错题本每道题只写三行:题目属于什么类型,我在哪里想错了,正确思路里最关键的判断是什么。
比如第 15 题如果我 WA 了,我会写:“类型:数字反转;错误点:没有判断负数,对 INT_MIN 取反溢出;关键思路:先用 long long,再单独存符号。”下次遇到类似题目,翻一下两秒钟就能想起来,比重新看一遍长篇代码有用得多。
这也侧面回答了为什么我不建议搜“郑州轻工业 OJ 答案”“东方博宜 OJ 答案 1065”这类关键词。别人给出的代码不一定理解你的错误点,直接抄过来,你只是拿到了一个 AC,失去了发现问题、修正思路的完整训练过程。真正想要答案,就把题目类型、自己的代码和错误提示记下来,自己对比解决。
5.2 一天刷多少题更合适
如果你每天能保证一到两小时,我的建议是每天 3 到 5 道新题,其中至少 1 道是自己花 20 分钟以上硬想出来的。第五天做到 13 到 15 题,正好处在这个量级。如果时间不够,只做一道难一点的题、把它的边界情况和输出格式完全吃透,也好过刷十道简单题。
还要提醒一点:刷题最怕的不是做得慢,而是断档。中间落了一两天,回来发现判断输入的方式忘了、格式化输出又不熟,心态很容易崩。宁可每天只写十几行代码,也要把“每天碰一碰 OJ”这个习惯续住。等过了第十天、第二十天,回看第五天这个阶段,你会发现自己对 WA 的恐惧已经淡了很多。
6. 关于第五天刷题的三点体会
最后说几句个人感受,也算不上总结,只是我在带着不少人做过这个阶段之后,最想强调的三件事。
第一,抄题解的时代在第五天就应该结束。前面几题你还可以通过模仿建立手感,13 到 15 题开始,模仿不来了,你必须有自己排错的能力。每 WA 一次,都对题目逻辑多一层理解,这种积累是题解给不了的。
第二,不要觉得“第五天才做到 15 题”很慢。OJ 刷题比的不是数量,而是稳定性和熟练度。我见过有人一天刷二十道,结果连循环边界都没真正理解;也见过有人一周只刷十几道,但每一道都能讲清楚为什么这么写。后者到了后期题目变难时,适应速度反而更快。
第三,把边界值测试当成肌肉记忆。这阶段练的三道题型,本质上都在反复训练同一件事:你写的代码能不能应付极端输入。这个习惯越早养成,后面遇到字符串、数据结构、搜索算法时就越省心。我在实际刷题中踩过几次坑之后,已经形成条件反射,代码写完先跑 0、1、负数、最大值四组数据,再跑正常样例。你也值得试试。