很多新生第一次打开 DHUOJ 的题库,基本都会在“基础 22、基础 23、基础 24”这三道题上卡住。说实话,这三道题本身并不难,难的是你第一次接触自动评测机,还没搞懂它跟你之间那套“冷冰冰的规则”。DHUOJ 这类在线判题系统,跟人工批改作业完全是两个物种:它不会看你代码写得漂不漂亮,只看你的程序能不能在限定时间内,把输入数据变成它预设好的输出。差一个空格、多一个换行、数组开小一格,结果就是红红的 WA 或 RE。
这篇文章就围绕 DHUOJ 基础题里的 22、23、24 三题展开,讲讲怎么分析题面、怎么确定算法、怎么写出能一次 AC 的代码,也把新手最容易踩的评测机坑位挨个列一遍。适合所有正在刷 DHUOJ 基础题的新生,也适合那些刚接触 OJ、被判题结果搞到怀疑人生的同学参考。
1. 先把 DHUOJ 这个“黑盒子”弄明白
1.1 为什么第一次刷题先要理解判题机制
DHUOJ 的全称是 Donghua University Online Judge,也就是校内自己部署的一套在线判题系统。很多课程作业、实验训练、甚至竞赛选拔都会用到它。它的工作流程很简单:你提交一份源代码,评测机自动编译,然后用一组或多组测试数据去跑你的程序,再把你的输出和标准答案逐字符比对。
关键就在“逐字符比对”这几个字上。人工老师看到你输出“3 ”可能觉得没问题,评测机不会这么想。它把你的输出拆成一个个字节,多了一个空格就是错,少了一个换行也是错。我见过太多学生,思路完全正确,最后挂在行末空格或者最后一行没有换行这种细节上,非常冤。所以刷 DHUOJ 之前,先花十分钟理解判题机制,远比多刷十道题更有价值。
1.2 判题结果到底在说什么
提交代码后,你会看到一个结果反馈,常见的有这么几种:
| 结果 | 含义 | 新手最容易犯的对应错误 |
|---|---|---|
| AC | Accepted,通过了全部测试点 | 恭喜,这道题结束了 |
| WA | Wrong Answer,答案错误 | 算法思路不对,或者输出格式有差异 |
| TLE | Time Limit Exceeded,超时 | 死循环、算法太慢、输入没读到 EOF |
| MLE | Memory Limit Exceeded,超内存 | 数组开太大,或递归栈溢出 |
| RE | Runtime Error,运行错误 | 数组越界、除零、野指针访问 |
| CE | Compile Error,编译错误 | 头文件缺失、语法错误、main 写错 |
| PE | Presentation Error,格式错误 | 主要是空格和换行的位置不对 |
| OLE | Output Limit Exceeded,输出超限 | 程序停不下来,疯狂打印 |
很多人看到 WA 就以为自己算法错了,其实 WA 和 PE 的界限在 OJ 上比较微妙。有些 OJ 会把多余空格算 PE,有些直接算 WA。你在 DHUOJ 上做题,只要记住一个原则:输出必须和题面“长得一模一样”,不许多心加东西。
1.3 提交代码前必须养成的三个习惯
第一,永远用标准输入输出,也就是 scanf、printf、cin、cout 这一套,不要在自己电脑上读文件。很多练习环境喜欢用文件重定向,但 OJ 提交时绝对不能带 freopen,评测机有自己的输入管道,你的 freopen 只会让它找不到文件然后 RE。
第二,main 函数必须写返回值。C 语言就写int main()并在最后return 0;,C++ 也一样。别嫌啰嗦,漏掉返回值在某些编译环境下会 CE。
第三,看清楚题目要求的语言。DHUOJ 有些基础题支持 C、C++、Java 甚至 Python,但不同语言的内存和时间限制可能不一样。新人阶段如果老师没特别要求,建议用 C 或 C++,因为这两类语言在 OJ 生态里最通用,别人给你排错也最方便。
2. 基础 22、基础 23、基础 24 的题型分析与思路拆解
2.1 基础 22:图形输出与循环嵌套
不同学期、不同老师导入的题包不完全一样,但 DHUOJ 基础分类里的第 22 题,绝大多数版本都是“给定行数 n,输出一个三角形”或者“输出一个菱形”这类图形题。以三角形为例,常见题面是:输入一个正整数 n,输出 n 行由*组成的三角形,第一行 1 个星号,第二行 2 个星号,依此类推。
这种题真正考的不是你会不会用printf("*"),而是你有没有建立起“逐行构造”的思维。你需要意识到:输出一共 n 行,第 i 行有 i 个星号,行尾换行。用两层循环就能解决,外层控制行数,内层控制每行星号数量。
#include <stdio.h> int main() { int n; while (scanf("%d", &n) != EOF) { for (int i = 1; i <= n; i++) { for (int j = 1; j <= i; j++) { printf("*"); } printf("\n"); } } return 0; }这里有一个非常重要的点:题目要求“多组输入”还是“单组输入”。很多学生不读题,看到示例只有一个输入就只写一次性处理,结果评测机给了多组数据直接 WA。上面代码用while (scanf("%d", &n) != EOF)就是让程序持续读入,直到文件结束。在不确定的情况下,多组输入的写法通常更稳妥。
如果题目升级成“输出等腰三角形”或“输出菱形”,思路也不难扩展。等腰三角形第 i 行需要先输出 n-i 个空格,再输出 2*i-1 个星号。菱形本质是上下两个三角形拼接,中间行别重复输出一次。这些变体其实就是循环控制条件的排列组合,多画几次就熟练了。
2.2 基础 23:字符串处理与输入缓冲区陷阱
基础第 23 题在 DHUOJ 里常以“统计一行字符串中某个字符出现次数”“统计数字字符个数”或者“统计单词数量”出现。这类问题的核心不是循环,而是字符串输入方式的选择。
最容易踩的坑是:用 scanf 读字符串,结果它遇到空格就停了。比如题目输入一行句子 “I love DHUOJ”,你可能只想读一整行,但 scanf("%s") 只读到 “I”。这时候要改用 gets 或 fgets。
#include <stdio.h> #include <string.h> int main() { char s[1005]; while (gets(s) != NULL) { int ans = 0; for (int i = 0; s[i] != '\0'; i++) { if (s[i] >= '0' && s[i] <= '9') { ans++; } } printf("%d\n", ans); } return 0; }注意,gets 在较新的 C 标准里被标记为不安全,本地编译可能警告,但 DHUOJ 的评测环境通常还能接受。更规范的写法是 fgets,不过 fgets 会连换行符一起读进来,统计字符时要注意不要让换行符干扰判断。我给新生的建议是:如果题目明确说“一行字符串”,先用 gets 解决,等以后刷题量上去了再细究 fgets 的兼容问题。
另外,如果你前面用 scanf 读了一个整数,后面马上用 gets 读字符串,中间那次的回车符会被 gets 吃掉,导致读到的内容为空。解决办法是在 scanf 后面加一个 getchar(),把换行符先吸掉。这个细节非常经典,基本每届新生都会遇到,我把它写在这里省得你再踩一遍。
2.3 基础 24:数组处理与排序输出格式
第 24 题最常见版本是“输入 n 个整数,从小到大排序后输出”。有些题包也会换成“去掉最大最小值求平均”或者“数组逆序输出”,但底层能力是一样的:你要会开数组、会遍历、会交换元素、会按指定格式输出。
很多人刚学编程时排序喜欢用最直观的选择排序或冒泡排序。选择排序的思路是:每一轮找到最小值放到最前面,n 个数就需要 n-1 轮。
#include <stdio.h> int main() { int n, a[1005]; while (scanf("%d", &n) != EOF) { for (int i = 0; i < n; i++) { scanf("%d", &a[i]); } for (int i = 0; i < n - 1; i++) { for (int j = i + 1; j < n; j++) { if (a[j] < a[i]) { int temp = a[i]; a[i] = a[j]; a[j] = temp; } } } for (int i = 0; i < n; i++) { printf("%d", a[i]); if (i < n - 1) { printf(" "); } } printf("\n"); } return 0; }这段代码里最值得关注的是输出格式。题目如果要求“每个数之间用一个空格隔开,末尾没有多余空格”,那你就不能傻傻地printf("%d ", a[i]),因为最后会多出一个空格。上面代码用了if (i < n - 1) printf(" ");的方式,把空格当成前导分隔符处理,而不是跟着每个数字输出。这个技巧在 OJ 刷题里很常用。
排序算法的复杂度对于基础题来说完全够用,n 一般不超过 1000。如果以后遇到 n 很大的题,就要考虑快排或者归并排序,甚至直接用 C 语言的 qsort 或 C++ 的 sort,这些是后话。当前阶段先把循环、交换、数组越界这些基本功练扎实。
3. 从读题到 AC:三道题的完整实操流程
3.1 拿到题面后第一件事不是写代码
新手最容易犯的毛病就是看到题面里有数字有例子,立刻打开编译器开始敲。我建议你先花五分钟做三件事:圈出输入约束、确认输出格式、手动模拟一遍样例。
输入约束决定了你的数组要开多大。比如题目说 n 不超过 1000,那数组开到 1005 就足够,多留几个位置防止边界越界。如果约束是 10^5,那就不能用太慢的排序;如果解空间很大,还要考虑 long long 而不是 int。这些信息全在题面里,只是很多人急着写代码,根本没注意到。
手动模拟样例也非常重要。比如第 23 题统计数字个数,样例输入是a1b2c3,你自己数一遍,确认答案是 3,再去写代码。这一步看起来傻,但能帮你提前发现自己对题意的理解是否有偏差。我见过不少学生,代码写得飞快,最后 WA 了才发现:哦,原来它要统计的是字母个数,不是数字个数。
3.2 在本地编译运行验证的步骤
拿到题后,我习惯先在本地把程序跑通,再提交到 DHUOJ。具体步骤是:
- 新建一个源文件,比如
test.c。 - 把前面想到的解法写成完整代码,补上头文件和 main。
- 在命令行里编译。Windows 上用 Dev-C++ 或 VS Code 的都行,命令大概是
gcc test.c -o test。 - 准备一份样例输入,比如第 24 题的
4 3 1 4 2,运行程序输入样例,看输出是否和题面一致。 - 如果一致,再想几组边界测试,比如 n=1、n=0、最大 n、全部相同数字。
边界测试特别重要。第 24 题如果输入只有一个数,你的排序循环还会不会正确输出?第 22 题如果 n=1,循环体执行几次?这些问题平时写代码不会注意,但评测机专门拿边界数据来试探你。
3.3 提交之后如何根据反馈继续排查
第一次提交就 AC 的人很少。看到 WA、RE、TLE 不用慌,先看反馈类型。
如果是 WA,优先检查输出格式:行末有没有多余空格、最后一行有没有换行、输出内容是不是多了调试信息。我见过很多学生把 printf 里的调试输出忘了删,直接提交,结果被 OJ 判 WA,这种错误相当气人。
如果是 RE,大概率是数组越界。检查所有数组下标是否可能跑到分配范围之外,尤其是字符串数组,别忘了要给结尾的\0留位置,定义 1005 就不要真的用满 1005。
如果是 TLE,先看是不是死循环,再用输入方式排查。很多人读多组数据不写while(scanf(...) != EOF),程序只处理一次就结束了,这种不会 TLE,反而会 WA;真正的 TLE 往往来自循环条件写反或者递归没有出口。
4. 新人在 DHUOJ 上最常踩的坑
4.1 编译错误大多不是语法问题
CE 看起来最吓人,但往往解决起来最简单。常见原因有:头文件拼错、main写成了mian、变量名用了系统保留字、末尾少写分号。把编译器的报错信息往上翻几步,找到第一个 error,基本就能定位。
还有一种低级错误是:在 C 语言代码里用了 C++ 特有的写法,或者反过来。DHUOJ 平台提交时会按你选择的语言类型编译,你在 C 语言提交页写了 C++ 语法,自然 CE。基础题阶段建议固定用 C 语言,等熟悉了再切换。
4.2 运行错误和数组越界的关系
RE 在基础题里九成是数组越界。有些同学定义int a[n]这种写法在部分编译器下能过,但数组长度必须是常量,不能是变量。更安全的写法是直接开一个足够大的固定数组,比如题目说输入最多 1000 个整数,就写int a[1005];,多的 5 个位置是给自己留的安全余量。
字符串处理里越界更隐蔽。你定义了char s[100];,但输入真的超过 100 个字符,gets 可能会把内容写到相邻内存里,程序可能不报错,但结果已经错了。遇到这类问题,把数组大小、循环边界都放大一点,是成本最低的防御手段。
4.3 格式错误与答案错误的区分
PE 和 WA 的区别可以这样理解:如果输出内容数字化结果完全正确,只是空格换行不对,那很可能是 PE;如果输出结果本身不对,那就是 WA。但 DHUOJ 有些题不单独区分 PE,会直接显示 WA,所以你不用纠结“到底是 PE 还是 WA”,只要严格对照题面要求的格式就行。
判断格式最靠谱的方法是:把评测机可能用的数据自己在本地跑一遍,然后用肉眼对比你的输出和题面示例。重点关注三处:数字之间用什么分隔、每行结尾是否换行、题目要求输出几行。这三处没问题,格式分基本就拿到了。
4.4 常见问题速查表
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 提交后 WA,样例却正确 | 边界条件没处理 | 测 n=1、n=最大、空输入 |
| WA 且输出内容明显奇怪 | 变量初始化遗漏 | 检查计数变量有没有归零 |
| RE | 数组越界或除以零 | 检查所有下标和分母 |
| TLE | 死循环或算法太慢 | 检查循环条件是否可能永不退出 |
| 读入字符串少内容 | scanf 遇到空格停止 | 改为 gets 或 fgets |
| 读整数后再读字符串为空 | 缓冲区残留换行 | 在中间加 getchar() |
| 输出多了空格或少了换行 | 输出格式不严谨 | 改用前导空格方式处理 |
5. 我在带新生刷 DHUOJ 基础题时的几条心得
5.1 先独立想半小时,再找别人问
很多同学卡在基础 22、23、24,第一反应是在群里直接喊“有没有代码发我一份”。这样做会让你的题白刷。OJ 刷题的价值不在 AC 那个结果,而在你从不会到会的过程。哪怕这半小时里你什么都没写,只是在草稿纸上画循环的流程,也比直接抄别人的代码收获大得多。
我自己带新生的时候定过一个规矩:一道题至少独立尝试四次,每次尝试都在代码上做标注,实在不行才允许求助。只要你能把“我做到哪一步”“我觉得问题出在哪”说清楚,再开口问人,效率会高非常多。
5.2 用对比法快速定位 WA 的根源
当你样例能过但提交 WA 时,除了调试输出,还有一个很实用的方法叫“对比法”。构造几组自己知道正确答案的小数据,比如第 24 题输入5 2 3 1 5 4,自己心算出正确输出是1 2 3 4 5,再用程序跑一遍,看输出跟你的预期差在哪。
如果差的数字完全对不上,说明逻辑有问题,重点检查交换和循环边界;如果数字都对但中间多了个空格,说明输出格式有问题。把问题归类之后,改起来就快多了。这个方法看着原始,但比漫无目的地猜测“哪里错了”靠谱得多。
5.3 基础题不要只满足于 AC
AC 之后我建议你再多想一步:这段代码还能不能更简洁?如果输入数据量变成一万倍,我的算法还能不能跑得动?第 24 题如果你用的是冒泡排序,可以考虑对比一下选择排序和冒泡排序的差异,再了解一下插入排序。第 22 题的三角形输出,如果改成倒三角,你的循环还能不能写出来?
这种“AC 之后再扩展”的习惯,才是基础题真正的价值。DHUOJ 基础题库里的题目不是让你背答案的,是让你通过刷题把循环、数组、字符串、排序这些基本功变成肌肉记忆。等你把这些基础题吃透,再往后刷数据结构或算法题时,就会轻松非常多。