1. 基础题3、5、6到底在考察什么:先看清OJ的门道
很多刚开始刷题的同学看到"基础 3 5 6"这种题号都会有点懵,不知道它对应的是什么样的题目,也不知道为什么这三道题会被放在一起。我最近把某高校在线评测平台(下面简称OJ)基础题库里的这三道题从头到尾重刷了一遍,才发现这个编号顺序其实是有讲究的:它们对应的正是OJ刷题最核心的三件事——输入输出格式处理、基础算法应用、边界情况排查。把这三道题吃透,后面再碰更复杂的题,起码不会因为低级问题卡到怀疑人生。
1.1 三道题在能力阶梯上的位置
先说第3题。它在题库里属于"输入输出练习"的范畴,表面上看只是求和,但真正让人犯错的点在于题目怎么描述输入数据。OJ和平时自己写程序最大的区别是,你面对的不是一个交互式输入,而是评测系统预先准备好的一批测试数据。程序要能从标准输入里读完所有数据,处理完,再按格式输出。很多第一次刷OJ的人会在这里翻车,原因就是没有意识到"多组数据"意味着什么。
第5题则是排序。它考察的不只是你会不会写排序算法,更重要的是你能否在规定的时限内处理完题目给的数据量。排序是所有算法题的地基,树状数组、线段树、二分查找这些高级主题,底层全是排序和有序性的思维。这道题我选择了手写排序,而不是直接调用库函数,原因后面会详细说。
第6题是字符串处理。字符串在OJ题里是出了名的"看起来简单、做起来全是坑",因为你要同时处理字符数组的读入、终止符、空白字符、ASCII边界等一系列问题。这道题我从零重构了读入逻辑,才把评测状态从"答案错误"刷成"通过"。
1.2 拿到题目第一件事:别急着写代码
我见过不少同学(包括早期的我自己)一看到题目描述末尾那句"输出样例即可",就开始噼里啪啦敲键盘。这是刷OJ的大忌。正确做法是先静下心分析三件事:输入以什么形式结束(EOF?特定位数的n?换行符?)、输出对换行和空格的要求是什么、数据范围是否大到需要改变算法策略。
以这三道基础题为例,它们的输入规模都不大,暴力解法完全可行,但输入格式的坑相当多。第3题要求你不断读入n,如果n等于0就终止;第5题第一行给整数个数m,第二行给m个整数;第6题输入一行字符串,可能包含空格。这些细节如果在写代码前没有理清楚,后面WA(错误答案)一次就要回去改半天代码。
所以我把刷题的第一个步骤固定成了:把题目描述复制到本地,用黑色加粗标出"输入格式"和"输出格式"两段,看完之后再动手。这个习惯看着不起眼,但帮我避开了至少80%的格式类错误。
2. 第3题:多组输入下的求和问题,第一步就卡住一半人
第3题的原题描述大概是这样的:输入包含多组测试数据,每组数据第一个数为n,表示本组有n个整数,当n为0时结束;每组输出所有整数的和。看起来是不是很简单?但这道题提交一次的通过率并不高,原因就出在最基础的读入方式上。
2.1 题目形态与隐藏边界
先说题目形态。n的取值题目里没有明说,但从基础题库的难度来推断,n大概率在100以内,整数范围也在int可承受的范围内。这意味着不需要开long long,也不需要优化算法,纯属考察你能不能把多组数据正确地读完、正确地和累加完。
隐藏边界有两处。第一处是"n为0时结束"——有些同学看到这一句,会真的把0当作一组数据去处理,于是程序卡死在读入循环里,或者输出一个多余的0。第二处是每组数据的输出后面要不要空行。OJ评测是逐字符比对的,多一个空格、少一个换行,都会被标记为"格式错误"(PE),虽然比WA好一点,但同样不能通过。
2.2 错误示范与正确写法
我最初提交的版本长这样,看起来没毛病,但就是WA:
#include <stdio.h> int main() { int n; while (scanf("%d", &n) != EOF) { if (n == 0) break; int sum = 0, x; for (int i = 0; i < n; i++) { scanf("%d", &x); sum += x; } printf("%d\n", sum); } return 0; }这个代码的问题出在哪?它在本地自己试的时候没有问题,但OJ里有一个测试点是n为0的情况终止循环,理论上也没问题。后来我排查了很久,才发现问题藏在scanf的返回值上:某些版本的OJ评测数据里会出现"只有n,后面没有数据"的情况,此时scanf("%d", &n)返回的是0而不是EOF,程序就直接进入死循环了。
正确的写法应该对scanf的返回值做严格判断:
#include <stdio.h> int main() { int n; while (scanf("%d", &n) == 1) { if (n == 0) break; int sum = 0, x; for (int i = 0; i < n; i++) { scanf("%d", &x); sum += x; } printf("%d\n", sum); } return 0; }这两种写法在日常本地运行中几乎看不出差别,但在评测系统的边界数据下,返回值的差异就是AC和WA的区别。这一点,是第3题教给我最深刻的一课:OJ的读入,永远不要用"想当然"的方式。
3. 第5题:排序问题的真相是"调库"还是"手写"
第5题是给出一组整数,要求从小到大输出。题目描述非常短,数据规模给的是m小于等于1000,看起来很基础。但在刷这道题的时候,我更关注的问题是:手写排序还是调用库函数?不同选择背后的代价完全不同。
3.1 为什么排序题容易把人带偏
资源少、时间紧的新手最容易走的一条弯路是:一看到排序就想直接调用qsort或sort函数。这样做本身没有错,在工程开发中我也会这么干,但在OJ基础题里,我强烈建议至少手写一次排序。原因有三点:
第一,基础题的意义就是让你理解算法本身。冒泡、选择、插入这三种排序,代码量都不长,自己写一遍能直观感受到比较次数和交换次数,这也是后面学习复杂排序算法的前提。第二,OJ评测讲究稳定,库函数的排序实现虽然强大,但如果你不理解它的比较器怎么写,反而容易在结构体排序上犯错,比如把降序写成升序,或者比较函数返回值不符合规则。第三,手写排序还能让你养成"手动模拟数据流"的习惯,这在排查WA时尤其重要。
3.2 手写选择排序和调用qsort的取舍
对m小于等于1000的数据量,选择排序的O(m²)完全够用。我实践下来的代码是这样:
#include <stdio.h> int main() { int m; int arr[1005]; scanf("%d", &m); for (int i = 0; i < m; i++) { scanf("%d", &arr[i]); } for (int i = 0; i < m - 1; i++) { int minIdx = i; for (int j = i + 1; j < m; j++) { if (arr[j] < arr[minIdx]) { minIdx = j; } } if (minIdx != i) { int tmp = arr[i]; arr[i] = arr[minIdx]; arr[minIdx] = tmp; } } for (int i = 0; i < m; i++) { if (i > 0) printf(" "); printf("%d", arr[i]); } printf("\n"); return 0; }这里有一个容易被忽略的输出要求:两个数之间以空格分隔,行尾不能有多余空格,但整行末尾必须有换行。我见过很多人在输出时直接写printf("%d ", arr[i]);,最后一个数后面自然会多一个空格,OJ就会判格式错误。
如果你确实想用库函数,也不是不行。C语言里用qsort,注意比较函数要写成:
int cmp(const void *a, const void *b) { int ia = *(int *)a; int ib = *(int *)b; return ia - ib; }C++里用sort更简单,sort(arr, arr + m);默认就是升序。但要记住,手写排序不是"笨",它让你在数据范围变大的时候,能自然想到归并排序、快速排序这些更优的方案。如果一上来就只会调库,后面遇到需要自定义排序规则的题,会比较痛苦。
4. 第6题:字符串处理的隐藏坑位,从一次WA说起
第6题看起来也不难:输入一行字符,统计其中数字字符的个数并输出。题干就一句话,没有特殊说明。但正是这种"没有特殊说明",让很多人在同一处栽跟头:这一行字符里到底有没有空格?
4.1 题目描述看似友好,陷阱在字符类型判断
如果这一行字符不包含空格,那么用scanf("%s", str)读入就够了;但OJ评测数据里往往混有空格,字符串变成类似"abc 123 def 456"这种形式。scanf("%s")遇到空白字符就会停止读入,后面的数字根本读不进来,统计结果自然出错。
当时我的思路是这样的:先用char数组存字符串,然后遍历每个字符,判断ASCII值在'0'到'9'之间就计数。逻辑没错,但我使用的读入函数错了。这就是典型的"算法对了,输入处理错了",整个程序等于白写。
4.2 排查链路:从读入方式到ASCII边界
我第一次提交的代码是:
#include <stdio.h> #include <string.h> int main() { char str[1005]; scanf("%s", str); int cnt = 0; for (int i = 0; i < strlen(str); i++) { if (str[i] >= '0' && str[i] <= '9') { cnt++; } } printf("%d\n", cnt); return 0; }本地测试"abc123"没问题,但我拿包含空格的测试样例一跑,发现输出完全不对。这就是WA的根源。排查过程其实不复杂,我用一个最简单的调试方式:把读入的字符串原样打印出来,看看它到底收到了什么。打印结果发现,我的程序只收到了"abc",后面的"123"根本没被读进数组。
正确的做法是使用gets或者fgets来读取带空格的一整行:
#include <stdio.h> #include <string.h> int main() { char str[1005]; if (fgets(str, sizeof(str), stdin) != NULL) { int cnt = 0; for (int i = 0; str[i] != '\0' && str[i] != '\n'; i++) { if (str[i] >= '0' && str[i] <= '9') { cnt++; } } printf("%d\n", cnt); } return 0; }这里要特别留意fgets的两个特点:第一,它会保留末尾的换行符'\n',所以遍历时要做判断,否则把换行符也算进去就会莫名少1个;第二,如果字符串长度超过数组容量,它会把剩余字符留在输入缓冲区里,所以数组尽量开大一点,题目没有给范围时,1000到2000都是安全的。
从这次WA里,我总结出一个排查链路:先打印读入的内容,确认输入处理没错;再打印计数结果,确认统计逻辑没错;最后检查输出格式。把这个顺序固化下来,字符串类题目的错误率会直线下降。
5. 基础题刷完之后,我踩过的坑和真正有用的经验
三道题刷完,我的评价是:它们不难,但非常值得做。因为它们展开了一个完整的OJ刷题视角——既有输入输出细节,又有算法手写训练,还有字符串处理技巧。更关键的是,它们让我踩了一串真实的坑,这些坑比多刷十道"套模板"的题还有价值。
5.1 新手最容易反复WA的五个细节
我把自己在刷题过程中踩过的坑整理了一张表,非常有代表性:
| 细节 | 错误表现 | 正确做法 |
|---|---|---|
| scanf返回值 | 死循环或漏读数据 | 判断== 1而不是!= EOF |
| 输出行尾空格 | 格式错误PE | 用辅助变量控制首尾空格 |
| 字符串含空格 | 只读到空格前内容 | 用fgets/gets读整行 |
| 换行符残留 | 字符串多出'\n' | 遍历时过滤'\n' |
| 数组越界 | 结果错误或崩溃 | 数组开大至少100到200 |
这些细节在教程里很难被当作重点讲,但在评测系统里,每一个都会让你的提交状态从AC变成WA或PE。
5.2 关于OJ刷题节奏的个人建议
最后分享一点个人体会。不要因为"这是基础题"就直接跳过,也不要一个晚上把三道题全部硬啃下来。我一个晚上只刷两道,刷完先不看别人的题解,自己复现一遍,再隔两天重新提交一次,看自己是否还记得写法的核心逻辑。
这种方法比一次刷十道题有效得多。因为OJ刷题的本质不是"我做过这道题",而是"我彻底理解了它背后的模式"。基础3、5、6这三道题,让我牢牢记住了"读入必须严格校验""输出格式必须精确到结尾无空格"这两个原则。在后面的进阶题目里,这两条原则又帮我挡住了一大批低级错误。
如果你也正卡在OJ基础阶段,建议你可以找找类似的题号,认真把输入输出这块地基打牢。刷题这条路没有捷径,但它有一个很友好的规律:前期踩过的每一个坑,都会在后面的题里以另一种形式回报给你。