一份面对DHUOJ(东华大学在线评测系统)基础题集的深度拆解。写这篇的初衷很简单:基本每届大一学生都会被OJ的22、23、24这三道题卡一下,但又很少有人把它们的共性规律讲明白。这篇文章我会把这三道题背后的判定逻辑、输入输出陷阱、常见扣分点全部摊开讲,尤其是我自己当年在这三题上反复提交才过审的经历,相信对正在刷题的你有直接帮助。
DHUOJ作为学校自建的Online Judge平台,题目编号越靠前,越偏向语法和基础算法。基础的22、23、24正好卡在一个微妙的节点:它不再像前面几题那样让你纯粹打印一个字符串,而是开始要求你接受外部输入、做计算、再按严格格式输出。很多同学就是在这一阶段第一次体会到“本地运行好好的,提交却报错”的滋味。
这篇文章真正适合两类人:一类是刚接触OJ、连题目判定状态(Accepted/Compile Error/Output Limit Exceeded)都还没认全的纯新手;另一类是已经能AC前面简单题,但遇到稍微复杂一点的输入格式就懵圈的备赛者。我会从题型规律、代码模板、避坑经验三个层面来讲,不卖关子,直接上干货。
提示:不同年级的DHUOJ题面可能有微调,但核心考点高度稳定,本文给出的思路对所有基础段题目通用。
1. 基础22、23、24的真正考点:从“会语法”到“会解题”
1.1 三道题在题目序列中的定位
DHUOJ的基础题序列通常是从“输出Hello World”级别的题目开始的,逐步递增难度。到了22、23、24这个编号区间,你仔细观察就会发现一个规律:它们已经不再直接给你完整的输出样本让你模仿,而是把问题描述成了一个“输入若干个数据,输出计算结果”的黑盒模型。
这个转变非常关键。前面那些题,你只要会用printf或cout就能过;但从这三道题开始,你的代码必须具备以下三个新能力:
- 读取未知数量的输入数据,并正确处理读取结束的情况
- 根据输入数据做一定逻辑运算,而不是无脑打印
- 严格按照题目规定的格式输出,多一个空格、少一个换行都会导致判错
很多同学在这三道题上栽跟头,不是因为不会写代码,而是因为“根本没想到OJ是拿隐藏数据来测你的”。你看到的题目描述只是冰山一角,Judge程序会把你提交的代码丢进一组你完全不知道的测试数据里,逐个比对输出结果。
1.2 隐藏数据的判定逻辑:为什么“样例能过”不等于“能AC”
这三道题里最让人抓狂的现象是:样例输入和样例输出明明能对上,点击提交却显示Wrong Answer。原因很简单——你得把代码放到更全面的测试集里去检验。
以我当年刷题的经验为例,OJ的测试数据通常会覆盖几种特殊边界:
- 最小数据:比如输入为0、1,或者负数
- 最大数据:比如输入的数值接近int上限
- 重复数据:比如大量相同的数字
- 杂乱格式:比如输入中间混着多个连续空格,或者最后一行没有换行
如果代码只针对样例数据做优化,或者某些变量类型选得不对,一旦碰到边界情况就会输出错误结果。基础22、23、24这组题中,最常见的问题就是整数溢出和使用int接收该用long long的数据。
// 错误示范:看似逻辑正确,但遇到大数会溢出 #include <stdio.h> int main() { int a, b; while (scanf("%d %d", &a, &b) != EOF) { printf("%d\n", a + b); } return 0; } // 正确示范:用 long long 接收,稳妥处理大整数 #include <stdio.h> int main() { long long a, b; while (scanf("%lld %lld", &a, &b) != EOF) { printf("%lld\n", a + b); } return 0; }1.3 三道题背后的“递进关系”
我在帮同专业学弟学妹看代码的时候发现,基础22、23、24实际上是有意安排成递进结构的,它们分别对应了三个不同的能力层级,我做了个简单的对照:
| 题目编号 | 核心考察点 | 常见出题方向 | 主要坑点 |
|---|---|---|---|
| 基础22 | 循环与控制流 | 累加求和、数列计算 | 循环边界、数据类型选择 |
| 基础23 | 数组与元素处理 | 极值查找、冒泡排序、去重 | 数组下标越界、循环范围 |
| 基础24 | 字符与格式处理 | 字符串倒序、字符过滤、ASCII码操作 | 缓冲区遗留换行符、输出格式 |
这个递进关系并非偶然。22题让你掌握“反复做一件相同的事”的循环思维,23题让你把数据装进容器并进一步加工,24题则从数值处理跳到字符处理,考察你是否具备切换输入类型的能力。一关一关迈过去,你的OJ刷题才真正算入了门。
2. 基础22:循环与累计的套路化拆解
2.1 最可能的题面与通用解题框架
虽然我无法保证你手中那份题面上写的是哪道题,但从多年各种OJ题库的规律来看,基础22这一题通常与“数列求和”脱不开关系。常见的考察形式有:输入一个n,输出1加到n的和;或者输入若干个整数,求其中所有数的总和;以及进阶一点的分组求和。
不管是哪种变形,核心解法都是一套循环累加框架,我把这个框架拆成四步:
- 读取一个启动变量(可能是n,也可能是EOF标记)
- 初始化一个累加变量为0
- 进入循环,反复读取数据并累加
- 循环结束后,按照题目要求的格式输出结果
这里有一个非常重要的经验:初始化累加变量为0,不是随便写写,而是为了防止“脏数据”。有些同学图省事,让累加变量沿用声明时未初始化的值,在本地编译器里可能恰好是0,但换个环境可能就是随机值,最终导致结果完全错乱。
#include <stdio.h> int main() { int n; long long sum = 0; // 累加变量务必初始化,且数据类型要够大 scanf("%d", &n); for (int i = 1; i <= n; i++) { sum += i; } printf("%lld\n", sum); return 0; }2.2 循环边界:小于等于还是小于?
别小看循环条件里这个“等于号”,它在22这类循环题里是分水岭级别的细节。求1到n的和,循环条件写成i < n,结果就变成从1加到n-1,直接少了一项;写成i <= n才是正确的。
我自己总结了一个快速判断方法:先拿小数据手算。比如题目说n=5,预期输出15,你把n=5代入循环里走一遍,如果循环体执行了5次,说明条件写对了;如果只有4次,说明少了等于号。这个方法很笨,但对样例数据非常有效,而且能帮你建立对循环次数的直觉。
还有一种情况很阴险:题目给的样例输入里n是5,你的i < n恰好因为某些原因算出了15以外的错误值,但只要你和正确答案差得不多,在OJ的Wrong Answer提示下你就会琢磨半天。所以我的建议是,循环边界这种细节,不管多简单,都要养成“先手算一遍”的习惯。
2.3 关于EOF读取方式的最佳实践
在基础22这题中,有些版本会要求处理多组输入数据,直到文件结束才停止。此时你用while(scanf(...) != EOF)还是while(cin >> n)取决于语言习惯,但有一个共同的注意事项:千万不要在循环内部对读取失败的情况做多余的假设。
我见过有同学这么写:
while (scanf("%d", &n) == 1) { // 处理过程 }这个写法本身没问题,scanf返回值为1表示成功读到一个整数。但有同学在while循环里再次调用scanf去读取下一组数据,导致每次循环漏读半个数据。这类错误在本地测试时很难发现,因为样例输入往往组数不多,多读一次碰巧也能读取到下一组开头的数据。
正确的做法是:一次性在循环条件中完成读取动作,循环体内只做业务处理。这既保证了不会漏读,也避免了误读。
3. 基础23:数组操作的边界思维
3.1 从“单点计算”到“容器处理”
如果你已经顺利AC了基础22,那么基础23会给你一种“明显上难度”的感觉。因为它不再是“读一个数,处理一个数,输出一个数”那么简单,而是要求你先读入一整批数据,存进数组,然后再从数组里做筛选、交换、或统计。
这种转变的核心在于:数据不再是一次性消费的流水,而是可以反复访问的集合。
在我个人的刷题经验中,基础23最让人头疼的不是数组的语法,而是访问数组时对下标范围的失控。比如题目要求“有n个整数,找出最大值和最小值”,你没注意数组长度,直接开了int a[10],结果n可能到20,一跑就数组越界。在本地编译器里,越界可能不会立刻报错,只是悄悄改写相邻内存;但提交到OJ上,就可能出现莫名其妙的Runtime Error。
#include <stdio.h> int main() { int n; scanf("%d", &n); int a[n]; // 变长数组,C99标准支持,按输入动态确定大小 for (int i = 0; i < n; i++) { scanf("%d", &a[i]); } int max = a[0]; int min = a[0]; for (int i = 1; i < n; i++) { if (a[i] > max) max = a[i]; if (a[i] < min) min = a[i]; } printf("%d %d\n", max, min); return 0; }注意:如果OJ编译环境严格遵循C89标准,变长数组可能不被支持。稳妥做法是开一个足够大的固定数组,比如
int a[1000],然后只使用前n个元素。这是我在DHUOJ上吃过亏之后学到的教训。
3.2 极值与排序的典型错误:忽略重复元素
基础23还有一种热门变形:让你把n个数排序输出。如果你会写冒泡排序或选择排序,这题本身难度不高;但有一个隐蔽的陷阱是“重复元素”。有些题目会故意输入包含重复值的数组,并要求输出排序后的完整序列。很多同学在排序算法实现时,遇到值相等的情况会做出错误交换,导致输出顺序不稳定,最终被判定为Wrong Answer。
我记得当时有个同学写的选择排序里,判断最小值的条件是<,在遇到重复最小值时下标一直不更新,结果多轮排序后重复元素的位置错乱,但打印出来的数字序列看起来又好像没问题。这种错误非常难发现,唯一的排查方式就是在纸上手动跑一轮数据。
我列一下自己用着最顺手的排序处理思路:
- 冒泡排序:每一轮把最大的元素“浮”到末尾,只需两层循环
- 选择排序:每一轮找出未排序部分的最小值,与未排序部分首元素交换
- C语言快捷方式:直接用
qsort函数配合cmp回调,同时注意返回值写法
3.3 输入读取与数组索引的对应关系
基础23里第二常见的错法,是把数组下标从1开始,但循环遍历时从0开始,或者在读取时从0开始但输出时从1开始,导致输出结果中少了一个元素、多了一个未初始化的垃圾值。
建议养成一套固定的书写习惯:读取和遍历都从0开始,数组下标最大用到n-1。这种操作虽然在C语言里和“从1开始”的直觉有点不同,但它能最大程度地避免混乱。如果你执意要从1开始存放,那遍历时也要保持从1到n,千万不要混着来。
4. 基础24:字符与格式处理的两个隐形坑
4.1 缓冲区混合输入:字符为什么读不到
基础24大概率会把方向切换到字符处理上,比如统计一段字符串中某个字符的出现次数、把字符串倒序输出,或者过滤掉非字母字符。这类题目本身的逻辑并不复杂,真正让大批人交白卷的,而是C语言中处理字符与数字混合输入时的缓冲区问题。
想象这样一个场景:先输入一个整数n,然后再输入n个字符,最后把这n个字符倒序输出。如果你直接用scanf("%d", &n)读取整数,接着用scanf("%c", &ch)循环读取字符,你会惊讶地发现:第一次读字符时,竟然读到了一个换行符。这是因为您按下回车后,换行符'\n'还残留在输入缓冲区里,被下一个scanf("%c")直接吃掉了。
4.2 彻底根治缓冲区残留:getchar的妙用
解决这个问题最常用的方式有两种:
第一种,在读取整数后主动吃掉残留的换行符:
scanf("%d", &n); getchar(); // 吃掉输入流中的换行符 for (int i = 0; i < n; i++) { scanf("%c", &ch); }第二种,使用scanf(" %c", &ch),在格式符前面加一个空格。这个空格能让scanf自动跳过所有空白字符(包括空格、换行、制表符),从而直接读到真正的字符。
这两种方法我都在DHUOJ上验证过,都能稳定通过判题。但个人更推荐第二种,因为不需要额外记忆“什么时候该加getchar”,只要在读取字符时养成加空格的习惯就行。
4.3 输出格式:空格与换行的“和珅式”考究
字符处理题的另一个隐形坑是输出格式。很多时候题目要求“输出每个字符,字符之间用空格隔开”,但很多同学在循环里无脑地每输出一个字符就加一个空格,导致最后一个字符后面多了一个尾随空格。在OJ判定中,多余的尾随空格和缺少换行一样,都会直接判为Presentation Error或Wrong Answer,非常冤。
我的处理套路是:判断当前元素是否最后一个,如果是,则只输出元素;如果不是,则输出“元素+空格”。或者更简洁一点,在第一个元素前不输出空格,之后的元素前都加一个空格。
写成代码大概是这个形态:
for (int i = n - 1; i >= 0; i--) { if (i == n - 1) { printf("%c", str[i]); } else { printf(" %c", str[i]); } } printf("\n");5. 从22、23、24延伸出去的刷题方法论
5.1 一套万能的题目拆解流程
你可能会觉得,不就是三道基础题嘛,AC掉就完了。但根据我带过的学生反馈来看,很多人在22、23、24这三题上耗费了大量提交次数,却依然没有总结出通用套路,以致于后面遇到稍微复杂一点的题就又开始手足无措。
所以我真正想借这三道题传递的不是答案,而是一套解题流程,我称之为“三步拆题法”:
- 读题后先圈定输入格式和输出格式,明确“程序需要处理几组数据”“数据以什么分隔”
- 确定算法核心:是循环累加,还是数组操作,还是字符转换
- 在本地测试中,除了样例数据,再手动增加一组边界数据,比如最小值和最大值,验证程序不会崩溃或溢出
这套流程听着简单,但真正执行下去很考验耐心。比如很多人输完样例看到输出正确,就兴冲冲地提交,结果被Wrong Answer打脸。如果你养成了“先测边界再提交”的习惯,大部分基础段的错误都能在本地提前拦住。
5.2 错题本的真正用法:记录判定状态而非只记题目
我在刷DHUOJ的初期,建了一个简单的文本笔记,每道错题记录三列内容:题目编号、第一次提交的判定状态、错误原因。后来我发现这个习惯的价值远超预期——因为人很容易在同一个坑里反复掉进去,比如“忘记初始化累加变量”“字符读取没跳过换行”“最后多输出了一个空格”,这些如果不刻意记录,下一次遇到相似题时大概率还会再犯。
记录判定状态还有一个额外好处:你可以逐渐摸清OJ的脾气。比如看到Compile Error,你就知道是语法问题;看到Runtime Error,就能猜出可能是数组越界或除零;看到Time Limit Exceeded,就说明算法复杂度太高,需要换思路。这些判断能力,都是靠一笔一笔记录练出来的。
5.3 如何判断是否进入下一题:不是AC就行
很多人觉得,OJ刷题就是“AC一道,下一道”,其实这个节奏对新手并不友好。以我的经验来看,评判一道题是否真正掌握,至少要满足两个条件:
- 你能在不看任何参考代码的情况下,把代码完整默写出来
- 你能跟身边同学讲清楚这道题的关键细节和潜在坑点
如果你只是照着题解敲了一遍,提交通过了,那其实意义不大。因为OJ题目成千上万,你最终要的不是某几道题的答案,而是处理问题的思维框架。22、23、24这三题恰好是框架养成的起点,走稳一点,后面会轻松很多。
6. 我在这些基础题上踩过的坑和最终建议
6.1 一个记忆犹新的“Presentation Error”教训
讲一个我自己的真实经历。大一刷基础22时,我第一次提交就看到了绿色的Accepted,心里还挺得意。但到了基础24的字符倒序题,我连续五次都因为输出格式不对被判了Presentation Error,本地运行却完全看不出问题。
后来我才发现,题目要求“每行末尾不能有多余空格”,而我恰好是在倒序输出时,把每个字符后都加了一个空格。系统认为你数字对了、顺序对了,但格式和标准答案有出入,就给你一个专门提示格式错误的判定。那次之后我再也没有犯过尾随空格的错误,但也因此长了个记性:在OJ面前,格式不是细枝末节,而是正确性的一部分。
6.2 建议初学者设立的小目标
如果你现在刚开始接触DHUOJ,我的建议是先别急着追求提交速度和AC数量。基础22、23、24这组题目本身就是绝佳的练习素材,你可以给自己定三个小目标:
- 目标一:三道题全部独立完成,过程中不参考题解,允许查阅语法手册
- 目标二:用至少两种不同的写法实现同一道题(比如C语言和Python各写一遍)
- 目标三:把每道题涉及的知识点用一个简单的思维导图整理出来,比如“数组越界”“缓冲区残留”“尾随空格”
这三个目标都完成之后,你会明显觉得自己的代码调试能力上了一个台阶,后面再遇到其他OJ平台的题目,也不会被陌生环境吓到。
6.3 最后一句话:保持随手测试的习惯
回到基础22、23、24这三道题本身,我相信你在完整看完这篇文章后,已经对它们背后隐藏的考点有了更清晰的认知。无论是循环边界、数组下标、还是字符缓冲区,这些坑都是可以提前避开的。但所有理论最终要靠实践来巩固,保持“写完就在本地多测几组数据”的习惯。
我的一个现实体会是:OJ刷题这件事,前期最大的障碍往往不是智商,而是粗糙。这三道基础题,本质上就是在训练你把代码写严谨的能力。你越早意识到这一点,越能避免在以后的算法题上因为低级失误反复重交。祝你在DHUOJ上顺利拿下这三个基础关,下一步的进阶之路会平坦很多。