如果你刚开始自学 ACM 基础题,第一次看到“2352、2549、2564、2565、2567、2572、2609、2607、2707、2708、2719、2721、2723”这一串编号,心里大概会犯嘀咕:这是不是哪个系统的随机排队号?我第一次拿到这份题单时也是这么想的。后来刷完才发现,这13道题不是简单的题目堆砌,而是一张按难度和知识点铺开的入门网。它们不要求你会特别复杂的算法,但要求你把输入输出、字符串、模拟这类基本功焊死。这篇文章不会逐题抄题解,我会把这批题背后真正值得练的东西、我踩过的坑、以及一套可以直接照抄的刷题节奏分享出来,适合刚入门 OJ、被 Presentation Error 折磨到崩溃的新手。
1. 这串题号看上去像随机数,其实是老手给新手挑的“组合拳”
1.1 题号大小和难度并不成正比
很多新手会下意识认为“编号越大越难”,刷到 2707、2708 的时候自动紧张。实际上在基础题阶段,编号只是入库顺序,不代表难度曲线。我刷到中间好几道题时发现,2609 附近的题甚至比 25 开头的某些题更容易读明白,难的从来不是题号,而是题面里藏着的边界条件。所以拿到题单后第一件事不是按编号硬啃,而是先把题目大概扫一遍,像看菜单一样看题型。
这批编号里,有需要处理字符串的,有纯粹考循环嵌套输出图形的,也有做数学模拟的。它们之间没有严格的先后依赖,完全可以根据自己的状态跳着做。但无论怎么跳,核心目标是一致的:让你在提交按钮面前养成“先想再写、先验再交”的习惯。我一开始就是吃了没做全局规划的亏,按顺序从 2352 一路往下做,结果在一道字符串题上耗了两个小时,把节奏全打乱了。
1.2 刷这部分题最先建立的,是“被 OJ 教做人”的承受力
在本地编译器里跑通样例,和在线评测系统给出 Accepted,完全是两码事。OJ 不看你写了多少行代码,只看你的程序在别人精心构造的测试数据面前会不会崩溃、会不会多输出一个空格、会不会在最后一组数据后多打一个空行。这种冷酷的评价机制,恰好是 ACM 基础题最值钱的地方。
我当时做这批题,前三天基本就是“编译错误”“答案错误”“输出格式错误”三个词反复抽打。一开始很打击自信,后来才明白,这种反馈是效率极高的老师。它逼着我学会看编译信息、学会手推样例、学会用最简单的数据自测边界。如果你也是刚接触在线评测系统,请做好心理准备:你不是在和题目打架,是在和“严谨”这个词打架。4352 也好,2723 也好,它们的价值就是让你在最短时间内把 C 语言基础里的坑踩一遍。
2. 真正卡住新手的,往往不是算法,而是“题面翻译”
2.1 输入输出格式就是题目的另一半
我在刷这批题目时发现,超过一半的“不会做”其实是“没读懂输入格式”。ACM 题面里有几个高频说法:多组输入、输入以 EOF 结束、每一组之间空一行、输出格式中的每一个字符都要严格匹配。这些描述如果不接触,看多少次都容易忽略。
比如“多组输入直到文件末尾”翻译成代码,就是最常见的while循环:
int n; while (scanf("%d", &n) != EOF) { // 处理一组数据 }有些题还会在每组输入之间夹一个空行,这时候不能直接把上一组的逻辑照搬,要先想清楚空行是交给代码处理,还是输入里本身就有。我在做编号靠近 2549 的某一题时,就一直以为输出要多打一个空行,结果反复提交都是格式错误,后来删掉多余空行才通过。这种浪费纯属没把“输入输出格式”当成题目的一部分。
2.2 一个样例的手算推演,能顶半小时胡思乱想
基础题最常见的失败模式是:看完题立刻觉得“这不简单吗”,然后直接写代码,最后程序输出和样例不一样,又找不出原因。破解这个问题的办法很笨但很有效:先把样例输入复制出来,拿一张纸,跟着题目描述手算一遍输出。
我在处理某道类似状态模拟的题时,原本以为只要用一个变量累加就够了,手算到第三组数据就发现累加结果和样例结果差了 1。回去重新读题,才发现题目要求的是区间左端不包含,而我看漏了这个细节。这个错误如果在代码里找,可能要盯十分钟;在纸上推,一眼就能看到逻辑断层。基础题阶段,千万不要觉得手算浪费时间,它其实是把自然语言翻译成代码的最短路径。
2.3 数据范围是隐藏的地雷
题目里经常出现一句话:数据范围不超过 2^31,或者结果可能超过 32 位整数。这句话很容易被扫一眼就略过,结果代码里用的是int,某组测试数据直接溢出,变成负的答案,然后你排查半天还以为是算法写错了。
我做这批题的习惯是,先把输入变量的数据类型用笔圈出来。只要出现“结果可能很大”“绝对值不超过 10^9”“累加多次”这类字眼,就默认用long long,不为省内存留隐患。还有一些题需要开数组,字符串长度明明只给了 100,数组却要开到 105 甚至 110,因为末尾还要放一个结束符 '\0'。这些看起来是小事,但基础题里十次 Wrong Answer,至少有一半和这些小事有关。
3. 刷完这13道题,我归纳出三类高频套路
3.1 字符串处理题:先解决“读进来”,再谈“怎么算”
这批题里,字符串处理的比重不小。有些题表面在讲故事,本质上就是让你对一串字符做统计、替换、过滤。处理字符串的第一道坎不是算法,而是“怎么把整行内容读进来”。scanf("%s") 会在空白字符处停下,如果题目要求读取包含空格的句子,它就不够用了。
我的做法是分情况选择读取方式:
| 场景 | 推荐写法 | 注意点 |
|---|---|---|
| 只读一个单词 | scanf("%s", str) | 不要写&,str本身就是地址 |
| 读一行可含空格 | gets(str) / fgets(str, len, stdin) | gets有安全隐患,fgets更稳 |
| 读单个字符 | scanf(" %c", &ch) | 前面空格用于吞掉换行 |
字符串读进来之后,常见的操作就是大小写转换。C 语言里不需要硬背 ASCII 码,可以直接用字符运算:
if (ch >= 'a' && ch <= 'z') { ch = ch - 'a' + 'A'; }用字符常量比较,比用数字 97 和 65 可读性好得多,也不容易抄错。我当时在处理某道字母统计题时,就是靠这个写法快速定位了大写字母集合。
3.2 图形输出题:行列坐标才是主角
图形输出题看着像纯体力活,实际考的是坐标感。比如让你打印一个由字符组成的对称图形,很多人的第一反应是“先打上半部分,再打下半部分”,但容易在空格数量上翻车。更稳的思路是先画出网格,用行号和列号的关系表示每个字符应该出现在哪。
以打印放大字母X为例,本质上就是判断当前位置 (i, j) 是否满足主对角线或副对角线条件:
for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (i == j || i + j == n - 1) { printf("X"); } else { printf(" "); } } printf("\n"); }这个方法比一条线一条线去凑空格清. Heck,清晰很多。图形输出题还有一个隐藏考点:行尾不能有多余空格。你在循环里顺手打印空格没问题,但如果每一行结束时多打了一个空格,OJ 通常会报 Presentation Error。所以循环里要控制好边界,要么用条件判断是否打印空格,要么在最后一列直接打印字符,换行之前不要再补空格。这批题里的图形题,我基本都用坐标法一次写对,靠的就是把“打印什么”和“在哪个位置打印”分开想。
3.3 数学模拟题:照着题目用手算一个小例子
数学模拟类的题,通常是给一个公式或操作规则,让你模拟若干次。这类题看着需要“数学天赋”,其实更需要的是耐心。我的经验是:先不要写代码,拿起笔,把样例输入代入规则,一步一步算到样例输出。这个过程中你会自然发现哪些变量需要额外记录,哪些条件需要加判断。
比如进制转换,核心是除基取余,但输出时要把余数倒过来。手算一遍 10 进制转 2 进制,就会明白为什么要用一个数组暂存结果再反向输出。否则直接跟着余数顺序输出,得到的一定是反的。下面是一个通用的进制转换框架:
int digits[100], cnt = 0; do { digits[cnt++] = n % base; n /= base; } while (n > 0); for (int i = cnt - 1; i >= 0; i--) { printf("%d", digits[i]); }用 do-while 而不是 while,是为了处理 n 一开始就为 0 的情况。这种细节不手算一次,很难第一时间想到。数学模拟题里,慢就是快,手算一遍样例至少能帮你省下三次提交失败。
4. 这13道题里最容易踩的五个坑,我全踩了一遍
4.1 Presentation Error 比 Wrong Answer 更让人想摔键盘
我第一次见到 Presentation Error 时,完全不知道是什么意思。后来才明白,它的意思是:你的答案内容基本正确,但格式和标准答案不一致。最常见的情况就是多打了空格、多打了空行,或者每两组数据之间没有空行。这类错误最气人,因为算法明明没问题,却拿不到 Accepted。
解决办法只有一个:把输出当成字符串对待,用“字符级”的眼光审查每个 printf。尤其是需要输出多组数据时,要不要在组与组之间插入空行,是整批题的高频考点。我的建议是,在写输出循环之前,先用文字描述一遍输出格式,比如“每组输出一行结果,组间空一行”,然后再决定空行的打印位置。这能避免在最后一组数据后又输出一个空行。
4.2 那个“吃掉换行符”的 scanf("%c")
这个坑几乎每个 OJ 新手都会踩。scanf("%d") 读完后,输入缓冲区里会留下一个换行符。如果紧接着用 scanf("%c") 读取字符,读到的往往不是想要的字母,而是那个残留的换行符。我就在做某道字符处理题时,连续三次发现输出里多了一个怪字符,后来把代码里的 scanf("%c") 改成 scanf(" %c"),问题立刻消失。
原因是 %c 前面的那个空格会告诉 scanf 先跳过所有空白字符。这是一个小到不能再小的写法差异,却能让你 debug 到怀疑人生。如果你发现自己的程序读入顺序混乱,优先检查是不是这里有残留字符在作怪。
4.3 字符数组永远多开一位
C 语言的字符串以 '\0' 结尾,如果你声明 char s[100],最多只能存下 99 个可见字符,因为第 100 位要留给结束符。很多题目声称字符串长度不超过 100,但你如果恰好开成 100,某组测试数据长度等于 100 的时候,程序就会越界,可能直接运行错误,也可能产生无法解释的输出。
这个坑我在这批题里踩过一次之后,就给自己立了个规矩:字符串相关数组一律在题目给的长度基础上加 5,甚至加 10。反正内存那么便宜,没必要为了省一个字节去和未定义行为搏斗。同理,用 strlen 得到字符串长度后,只遍历到 strlen(s)-1 即可,不要拿着长度当数组上界无脑往里面写。
4.4 循环输入不清空,上一次的数据还在
多组输入是 ACM 题的基本形态。如果你开了全局数组或局部数组,在每一组数据开始时没有重新初始化,上一组留下的脏数据就可能影响这一次的判断。尤其是统计字母出现次数的题,如果忘记清零计数数组,第二次统计结果就会把第一次的计数累加上去,答案自然错。
我的习惯是尽量把数组定义在 while 循环内部,这样每轮都会重新分配;如果必须用全局数组,就在每组数据开始时用 memset 初始化。不要嫌这一步多余,数据残留问题是最难排查的错误之一,因为它在第一组样例上可能完全正常,到第二组才突然发病。
memset(cnt, 0, sizeof(cnt));注意 sizeof 是对数组本身使用的,如果数组是传入函数的指针,就不能再用 sizeof,否则得到的是地址大小。这个细节我在早期也翻过车。
4.5 数据范围算错,int 变 long long 的一百种方式
ACM 基础题里,运算结果溢出是隐蔽的炸弹。比如两个 int 相乘,结果可能直接超过 2^31-1,如果继续存在 int 变量里,等到的就是一个莫名其妙的负数。有些题根本不给你说“数据很大”,要等你交一发 Wrong Answer 后自己去怀疑。
我在做某道带累加的模拟题时,第一次用 int 存储结果,小数据全对,数据稍大就负数,排查了十分钟才发现是累加量太大。改成 long long 后一次通过。从那之后我养成了一个习惯:所有可能累加的中间量默认 long long,只有明确知道不会超过 2^31 时才用 int。ACM 比赛里没有人会因为多用了一点内存表扬或批评你,但溢出一定会让你挂题。
5. 如果再来一次,我会这样安排这13道题的刷题顺序
5.1 先做图形输出,再做字符串,最后啃数学模拟
刷题顺序其实比刷题数量更重要。图形输出题对算法要求最低,考的是循环和坐标,适合用来建立“我能写对代码”的信心。字符串题开始引入输入输出细节,适合用来训练读题和调试。数学模拟题往往要处理边界条件,放在最后做,你已经有前面几题的警示,更容易避开数据范围、清零、换行符这些坑。
如果让我重新安排这13道题的顺序,我会把销售时间分成两轮。第一轮只求每个题有思路、能写出一个可以通过样例的版本,不追求一次提交成功;第二轮再针对提交失败过的题做集中梳理,把错误的类型记下来。
5.2 用时间盒限制死磕时间
刷基础题最常见的问题不是不会,而是在一道题上死磕太久,导致整张题单推进不下去。我的建议是:每道题给自己 45 分钟。如果 45 分钟还没有 Accepted,就允许自己看题解或题解思路,但看完之后一定要把它放在一边,重新自己写一遍代码。这个“重写”的动作比看十篇解析都有效。
45 分钟之后若仍有错误,先把代码放一放,去跑跑步或干点别的事,回来再调试。很多次我在放松间隙突然想起边界条件没处理,等回到电脑前改一行就过了。卡住的时间不会让代码自动变对,但放松之后的眼睛往往能看见原来的盲区。
5.3 一份属于自己的提交失败排查清单
刷完这批题后,我给自己整理了一张排查清单,每次 Wrong Answer 或 Presentation Error 都会按顺序过一遍,基本能解决 80% 的问题:
| 失败类型 | 排查顺序 |
|---|---|
| Wrong Answer | 样例是否手算过?数据范围是否溢出?数组有没有初始化?多组数据间是否残留? |
| Presentation Error | 行尾是否有空格?组间空行是否正确?最后一组是否多打空行? |
| Runtime Error | 数组越界、除零、栈过大? |
| Compile Error | 检查变量名、分号、中英文括号、是否缺头文件 |
这份清单是刷代码题最重要的产出之一。每遇到一个新坑,就往里面补充一条。刷到最后,你会发现自己不再害怕红色的错误提示,因为每一个提示你都知道下一步该去哪里看。我觉得刷 ACM 基础题的意义,不在于记住了多少算法模板,而在于最终形成了一套稳定应对“程序出错”的方法论。这串看起来像随机数的题号,只是那套方法论的引子而已。
最后再分享一个小技巧:准备一个空白的 txt 文件,专门记录你自己犯过的错误类型,不要复制网上的标准答案,只写自己的原始错误描述。等这批题全部刷完,回头翻一遍那个文件,你会看到自己是怎么一步步从“连换行符都管不住”变成“一眼扫过去就知道哪里会错”的。这个文件,比任何刷题数量统计都更值钱。