☰
OJ基础题复盘:110/111/112循环、素数判断与数组逆序的细节
2026/9/28 6:36:39 网站建设 项目流程

1. “3.02”的刷题现场:基础题也有值得复盘的东西

3月2号晚上,我把OJ上的基础题110、111、112连着刷完了。老实说,这三道题都算不上有难度,任何一个写过几十道题的人都能轻松拿下。但恰恰是这种“基础题”,反而最容易暴露问题——不是算法不会,而是输入输出、边界条件、数组下标这些基本功不扎实。很多同学刷题喜欢直接跳到中等难度,觉得基础题没营养,我原来也这么想,直到有一次陪着基础薄弱的同学刷题,才发现同样的题,不同人的写法差距能有多大。

这篇文章不打算给什么惊天动地的算法,而是把这三道题的完整思考过程、提交记录、踩坑和排查链路整理出来。无论你用的是东方博宜、杭电OJ、洛谷还是学校自建的OJ,考的点几乎都是一样的:循环、分支、数组、格式控制。110、111、112正好把这几样各覆盖了一遍。

适合谁看?两类人。第一类是刚接触OJ、连“Presentation Error”和“Wrong Answer”都分不清的新手,这篇文章可以帮你建立一套稳定的做题流程。第二类是已经刷了几十题但总在输出格式上丢分的半新手,这三道题里埋着好几个隐藏规则,理解了之后能少交很多次无效提交。

我当天刷题用的OJ是常见的Web端评测系统,编程语言选的C++,编译器G++。做题顺序没有特殊讲究,就是按题号从110开始。题目本身是分开的三个独立问题,但串在一起看,恰恰是一条从“会写”到“写得对”再到“写得稳”的进阶线。

1.1 为什么把110、111、112放到同一天做

如果你的OJ上也有类似编号的基础题,大概率是这样分布的:110是循环输出、111是条件判断、112是数组处理。

这三个知识点正好覆盖了绝大多数OJ入门题的考察范围,而且彼此之间有依赖关系——循环是最早接触的控制结构,分支是逻辑思维的起点,数组则是数据组织方式的第一次升级。一道一道分开看,知识点是孤立的;放在同一天做,你能明显感觉到难度不是线性上升,而是从“语法正确”慢慢变成了“逻辑正确”和“设计正确”。

1.2 我刷题前的固定动作:环境与约定

刷题前我习惯先做三件事:确认编辑器支持括号自动补全,默认C++14标准,然后打开题目列表把输入输出样例看一遍。

哪怕题目一看就懂,我也坚持把样例手动算一遍再写代码。这个习惯帮我拦住过很多次“自以为懂了”的翻车。以110题为例,样例给的n可能是5,输出“1 2 3 4 5”。如果你只盯着这个样例写,直接cout << i << " ",最后多一个空格,本地跑得完美,提交上去就是一个Presentation Error。

2. 第110题的输入输出细节:循环题最先卡的往往不是循环

110题在我刷的OJ上是这样描述的:输入一个正整数n,输出从1到n的所有整数,每个整数后跟一个空格。题目很短,短到很多人看一眼就开写。我见过最快的写法是这样:

for (int i = 1; i <= n; i++) { cout << i << " "; }

这段代码在本地打印出来的效果和样例一模一样,肉眼完全看不出任何问题。但提交上去很可能会收到一个PE(Presentation Error)。原因很简单:OJ的评测不是看人眼,而是逐字符比对输出结果。多一个空格、少一个换行,全都算错。很多OJ对这种“末尾多空格”的情况会提示格式错误,因为它不是逻辑错,是排版错。

2.1 题意定型与代码骨架

正确的思路是,先把输出格式定死:要么“数字之间用空格分隔,末尾无空格”,要么“每个数字后跟空格但最后统一处理”。我习惯用第一种,因为它在评测机眼里最干净。

#include <iostream> using namespace std; int main() { int n; cin >> n; for (int i = 1; i <= n; i++) { if (i > 1) cout << " "; cout << i; } return 0; }

这里最关键的代码是if (i > 1) cout << " ",它的意思是:除了第一个数字,其余数字前都补一个空格。这样最后一个数字后边不会有空格,而且不需要额外判断i == n的情况。

你可能觉得这个技巧太基础,但基础题的意义就在这里:把这种“条件式分隔符”的思路练成肌肉记忆,后面写数组输出、矩阵输出、链式拼接时全部沿用同一个模式,一次都不会再犯格式错误。

2.2 输出格式的两个隐形扣分点

110这种题,除末尾空格之外,还有一个许多人忽略的点:输入结束后的换行。

OJ评测对换行的要求没那么苛刻,绝大多数题目末尾多一个换行不会影响判定。但有一种情况例外——如果题目明确要求“每行输出一个结果”,而你用了循环输出但忘记换行,那就会把所有数据挤成一行,直接WA。

所以我平时写题会给自己定一个死规矩:每一次输出之后,明确知道当前光标停在哪个位置。输出一个数字,停在数字后;输出完一行,补一个换行。这件事听起来简单,但实际写复杂题时,十几个输出语句叠在一起,很容易搞混,基础题就是练这个意识最好的场合。

2.3 边界值测试方法:n=1, n=10

代码写完不要急着提交,先在本地跑几个特殊输入。

第一个测n=1。如果程序输出“1 ”(1后面带空格),格式上看起来还行,但你以为的“空格在末尾”在评测机眼里就是问题。第二个测一个中等大小的值,比如n=10,肉眼检查数字之间是不是只有一个空格。第三个测较大的值,比如n=100000,观察程序是否能在1秒内跑完——110题这种规模其实不存在性能问题,但这个测试习惯要养成。

我当天第一次提交110时,第一版用的就是开头那种“每个数字后带空格”的写法,结果是Presentation Error。看到这个结果我反而笑了,因为这种错误往往说明逻辑没问题、只是细节不到位,改成条件式分隔符之后立刻AC。

3. 第111题的判定逻辑:条件怎么写才不会漏判/误判

111题在我用的OJ上是素数判定:输入一个整数x,判断它是否为素数,输出YES或NO。这题比110多了一个维度——它不仅要你输出东西,还要你先做出一个“判断”。而判断题的坑,几乎全部隐藏在条件边界的处理上。

第一次看到这题,很多人的反应是:这有什么难的?从2循环到x-1,只要有能整除的就输出NO,循环完都没找到就输出YES。这个写法在x比较小的时候没有任何问题,但它存在两个隐患:一是当x是1或者负数时,循环根本不会执行,程序会直接输出YES;二是当x很大时,循环x-2次,评测机可能要超时。

3.1 题目常见形态与分支建模

先说边界。素数在数学上的经典定义是“大于1的自然数中,除了1和它本身以外不再有其他因数”。所以x=1不是素数,x=0也不是,x=负数更不是。但很多教材的范例代码根本没提这个,导致提交之后挂在边界测试上。

我的建模思路是三步:

  1. 先排除所有“显然不是素数”的情况:x小于等于1,直接输出NO。
  2. 再排除“显然是素数”的特例:x等于2,直接输出YES。
  3. 剩下的x从3开始,只用检查2到sqrt(x)之间有没有能整除的。

第三步的原理,我在下面单独说。先看一个容易踩的细节:当x是偶数且大于2时,一定不是素数。如果你在进入循环前加一个判断if (x % 2 == 0) return false,整个循环次数又少一半。

3.2 我第一次WA的排查链路

我当天写111的时候,第一版是这样的:

for (int i = 2; i < x; i++) { if (x % i == 0) { cout << "NO"; return 0; } } cout << "YES";

没写x<=1的特判。提交之后,前面几个测试点都过了,最后一个隐藏测试返回了WA。我当时的第一反应是“怎么可能,循环逻辑没错啊”。然后我把评测机返回的结果和我的本地输出放在一起对比,发现WA也就意味着某个测试用例的输出和预期不一致。

排查过程是这样的:先把x=1、x=2、x=9、x=97一组一组列出来手算,然后逐一在代码里测试。x=9没问题,x=97没问题,但x=1输出了YES,正确答案是NO。问题立刻定位了。

这个排查链路值得新手记一下:不要盯着代码猜测,先把可能出问题的输入列成一张表,然后看程序在哪个输入上表现异常。一旦找到那个异常输入,根因基本就浮出水面了。这比在代码里加三行注释自我怀疑要高效得多。

3.3 优化思路:从暴力到根号剪枝

暴力循环到x-1,时间复杂度是O(n)。对于x=10^9这种输入,循环十亿次,本地都要跑好几秒,评测机大概率直接TLE。但如果你稍微了解一点因数成对出现的性质,一切就简单了:

如果x有一个大于sqrt(x)的因数a,那么x必然有一个小于sqrt(x)的因数b,因为a * b = x。也就是说,只要检查2到sqrt(x)区间内是否存在因数,就能判断整个数是否为素数。

bool isPrime(int x) { if (x <= 1) return false; if (x == 2) return true; if (x % 2 == 0) return false; for (int i = 3; 1LL * i * i <= x; i += 2) { if (x % i == 0) return false; } return true; }

这里有三个小细节,任何一个都能让你翻车:

  • 1LL * i * i <= x,用long long防止i*i溢出。当你把循环上限从x改成sqrt(x)后,x可以放宽到10^9甚至更大,int相乘会爆。
  • i += 2,因为已经排除了偶数,奇数因数只用遍历奇数即可。
  • 判断条件用i*i <= x而不是i <= sqrt(x),因为浮点数sqrt有精度损失,在整数比较里这种事很容易出玄学错误。

4. 第112题背后的数据与逆向思维

112题在我这的OJ上是数组逆序输出:第一行输入一个整数n,第二行输入n个整数,要求从最后一个开始依次输出,空格分隔。

这题的核心难点不是“逆序输出”,而是“如何不改变原数组的情况下逆序输出”以及“如何理解数组下标和位置的关系”。很多新手一看到逆序就想到交换数组元素,其实这个需求根本不需要交换。

4.1 从正向处理到逆序输出的思路转换

正向输出一个数组是for (int i = 0; i < n; i++),逆序输出则是for (int i = n - 1; i >= 0; i--)。差别只有三处:初始值从0变成n-1,循环条件从i<n变成i>=0,遍历方向从加变成减。

看上去简单,但对刚接触数组的新手来说,这里有一个思维拐点:下标从0开始。数组的最后一个元素不是下标n,而是下标n-1。这一句话能拦住很多人——我见过不止一个同学把逆序输出写成for (int i = n; i > 0; i--),然后发现第一个输出的是数组隔壁的垃圾值。

4.2 数组越界、初值与清理

如果写成for (int i = n; i > 0; i--),访问的是a[n]。C++的数组越界不会立刻崩溃,尤其你在本地编译器上跑,可能碰巧那个内存位置是个0,输出出来也看不出大问题。但评测机的运行环境不同,相邻内存可能是任意值,于是出现时对时错、换台机器就WA的诡异现象。

我的建议是:凡是涉及数组的题目,代码里先明确下标范围,边写边默念“左闭右开”。数组a[0]到a[n-1]是合法区间,任何下标出了这个区间都是越界,不需要考虑“是不是碰巧能跑”。

另外,数组的初始化也很容易被忽略。如果你声明的是局部数组int a[100005];,里面可能是随机值。虽然这道题每个元素都会被输入覆盖,不怕脏数据,但养成int a[100005] = {0};的习惯不会有坏处。后面遇到“只统计部分位置”的题,这个习惯能救你一命。

4.3 评测机视角:时间与内存的初次感知

112题如果数据范围是n<=100000,循环100000次,无论怎么遍历都是瞬间完成,不存在性能压力。但我建议你在做完之后做一个动作:把n改成1000000,再在本地测一次运行时间。

这不是必须的步骤,但它能帮你建立“数量级直觉”。以后遇到n=10^5的题,你就能本能地判断O(n)算法没问题,而O(n^2)算法会超时。这种直觉光靠背复杂度分析是建立不起来的,必须通过实测体会。

我当时提交112,第一版用的是交换数组元素再正序输出的写法,也AC了。但复盘时我意识到:交换操作需要遍历一遍、输出还需要遍历一遍,而直接倒序输出只需要遍历一遍。逻辑更简单,运行也更快。基础题的价值就在这——给你机会去对比不同解法之间的差距,而不是只有一个正确答案。

5. 基础题之后的十条实用建议:平台、节奏与心态

三道题复盘完,我想分享一些更“虚”但同样重要的东西——刷OJ的平台选择、节奏安排和心态建设。这些东西不属于任何一道题,但决定了你能不能把刷题这件事坚持超过一个月。

5.1 选OJ的参考维度

新手面对的第一个问题是:去哪个OJ刷题?

网上讨论最多的几个平台,各有侧重。杭电OJ题目数量多、涵盖面广,很多经典题在面试和竞赛里都能看到影子,缺点是老题风格偏竞赛化,对新手不够友好。东方博宜这类青少年编程OJ,题目组织更偏向教学顺序,从基础到进阶排列清晰,适合零基础起步。学校自建的OJ(比如郑州轻工业大学OJ、杭师大OJ)通常会绑定平时作业和期末考试,优先保证把课程布置的题目吃透,比盲目刷外面的题更实际。

我的建议是:以学校OJ为主,以公共OJ为辅。学校OJ的题量和难度排序一般和课程进度挂钩,你不用费心安排学习路线。公共OJ则用来开阔视野、补充练手。

5.2 建立自己的错题本

很多同学刷题是“AC之后立刻下一题”,这样效率其实很低。第二天你再看到同一道题,大概率连当时踩过的坑都忘了。

我的做法是给每道做错的题记一行笔记:题号、错误类型、根因。比如“PE:末尾空格”“WA:x=1未特判”“TLE:暴力循环到n而不是sqrt(n)”。这个错题本不用很精致,微信文件传输助手或者手机备忘录都行,关键是每次提交前翻一眼——同一类错误如果连续犯两次,那说明不是不会,是没走心。

我自己的统计是:格式错误大概占了前50道题所有错误的三分之一。这比例高得离谱,但也证明了一件事:基础题刷得值,血的教训越早经历越好。

5.3 什么时候该升级难度

当你发现110、111、112这类题已经能一遍AC、不需要调试时,就可以往前走了。升级的标准不是“做对了”,而是“稳定一次做对”。如果十道基础题里还有两三道会卡在输出格式或者边界条件上,那就继续练,别急着碰中等题。

刷到中等题之后,你会遇到全新的挑战:思路想不出来、想到了但写不出来、写出来但超时。那时候再回头看你今天刷的三道基础题,才会意识到它们真正的意义——循环、分支、数组这些最底层的东西不是“简单”,而是“地基”。

最后说一点我个人的体会:我刷这三道题,总共花了一个多小时,其中一半时间花在WA之后的排查上。如果只看AC数量,效率很低;但如果看学到的东西,我发现自己在“输出格式控制”和“边界条件思考”这两件事上,比刷十道中等题还要有收获。基础题的正确打开方式不是求快,而是求稳。把每一道看似简单的题写得滴水不漏,才是往后所有复杂题最扎实的起点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询