如果你跟我一样,最近在带着刚入门编程的学弟学妹刷OJ,那一定绕不开“基础7-9题”这个位置。在很多高校自建的在线评测系统里(我们这届学生私下喜欢把自己学校那个OJ叫“东华OJ”,算是一个小圈子的习惯叫法),第七、第八、第九这三道题并不是最难的,却是每年卡人最多的地方。不是题目本身多可怕,而是这题集中了OJ最典型的几个“坑”:多组输入、格式化输出、边界条件、评测机判定逻辑。
这篇文章我会把这类OJ基础7-9题做一次完整拆解。重点不是给你一份“背下来就能过”的代码,而是讲清楚每一题背后的出题意图、判定规则和常见误区。无论你用的是C语言还是Python,核心思路都一样。我自己在带新人过程中发现,把这三题吃透,后面刷题会顺畅很多。
1. OJ基础7-9题整体设计与思路拆解
1.1 为什么是第7-9题,而不是第1-3题
绝大多数OJ平台的基础题板块,前几道题一定是“A+B”“交换变量”“判断奇偶”这种热身题。到了第7-9题,难度会有一个非常微妙的跳跃:题目不再是“单次输入、直接输出”,而是开始要求你循环读入、多次处理、按规则格式化输出。
我比较过好几个高校OJ的基础题列表,第7-9题几乎都落在三类经典题型上:
- 最大公约数 / 最小公倍数计算
- 回文数判断
- 数组排序(或者九九乘法表这类双重循环输出)
为什么偏偏是这三类?因为它们分别考察了三个不同的基本功:数学建模能力、数字处理与边界判断能力、循环嵌套与输出控制能力。这正是OJ从“热身”过渡到“正式题目”的分水岭。你把这几个能力补上,后面再遇到字符串处理、模拟题,才不会一上来就崩。
1.2 这三道题的共同隐藏考点
基础7-9题里最容易被忽略的,是它们几乎都涉及到“多组输入”。很多新生在同一道题上栽跟头,不是因为算法不会,而是因为他们只写了“读一次数据,算一次结果”的代码,遇到OJ评测时反复报错却找不到原因。
在这类OJ题里,输入通常长这样:
6 9 12 8 1 1这意味着程序要持续读取数据,直到输入结束。对应C语言就是while(scanf(...) != EOF),对应Python就是while True + try/except。这一点是第7-9题区分“刷题老手”和“新手”最重要的隐性条件。
再一个共同点是输出格式的精细度。题目会要求“每个结果占一行”“乘积之间用一个空格隔开”“输出结束后没有多余空行”。很多人在本地IDE里看着完全正确,提交却提示Presentation Error(输出格式错误),就是因为多打了一个空格或少打了一个换行。
1.3 用生活类比理解OJ的判定逻辑
你可以把OJ的评测过程想象成一次“严格的口算验收”:程序读取标准输入,像考生看题;然后计算出结果,像考生答题;最后评测系统把它的标准答案和你的输出做逐字符比对,多一个空格都算错。
这就好比你去填一张表格,内容全对,但姓名栏多了一个空格,系统也会判“无效”。所以刷OJ基础题,第一课不是学算法,而是学会“输出洁癖”——每一行的每个字符都必须和题目要求完全一致。
2. 第7题:最大公约数与最小公倍数的两种写法
2.1 题目内容与核心考点
这类OJ基础第7题,最常见的出法是这样:
输入两个正整数a和b,输出它们的最大公约数和最小公倍数,每个结果占一行。
表面上考察的是数学公式,实际上考察的是辗转相除法。辗转相除法的核心是:
gcd(a, b) = gcd(b, a % b)直到余数为0时,上一次的除数就是最大公约数。这个定理不需要死记,你可以在纸上自己推一遍:比如34和18,先算34%18=16,再算18%16=2,再算16%2=0,所以最大公约数是2。
为什么要用辗转相除法而不是从1开始枚举?因为当a和b很大时(比如1亿和9千万),枚举的效率会非常差,OJ评测是有时间限制的,通常1秒左右。一个简单的规律:基础题默认会让你考虑最大数据范围,不优化就容易超时。
2.2 最小公倍数的计算顺序要小心
最小公倍数有一个很常用的公式:
lcm(a, b) = a * b / gcd(a, b)这个公式本身没错,但实际写代码时我强烈建议你写成:
long long lcm = a / gcd * b;而不是:
long long lcm = a * b / gcd;原因很简单:a * b中间结果可能溢出。假设a和b都在int范围内(最大约21亿),两者相乘就超过int上限了。哪怕你声明了long long,如果你先写了a * b,这一步仍然可能按照int规则溢出,然后再转成long long已经晚了。
这在OJ里是一个非常经典的“本地对、OJ错”案例。我见过不止一个同学在本地用很小的测试数据跑,结果正确,一提交就WA,就是因为数据类型和数据范围没考虑到。
2.3 用C语言实现的参考写法
#include <stdio.h> int gcd(int a, int b) { while (b != 0) { int temp = a % b; a = b; b = temp; } return a; } int main() { int a, b; while (scanf("%d %d", &a, &b) != EOF) { int g = gcd(a, b); long long l = (long long)a / g * b; printf("%d\n%lld\n", g, l); } return 0; }这里要注意几个细节:
scanf的返回值是成功读入的参数个数,如果读到文件末尾会返回EOF,所以while(scanf(...) != EOF)是标准写法。%lld是long long的输出格式,很多人写成%d,就会一直输出错误结果。- 每输出完一组结果,后面要有一个换行。有些题会要求“两组结果之间空一行”,但基础题通常只要求换行,看题目要求来。
2.4 Python版本参考
import sys def gcd(a, b): while b: a, b = b, a % b return a for line in sys.stdin: line = line.strip() if not line: continue a, b = map(int, line.split()) g = gcd(a, b) l = a // g * b print(g) print(l)Python的好处是整数没有溢出问题,所以可以放心先除后乘。但要注意sys.stdin读到的每一行末尾有一个换行符,strip()之前如果直接split()其实也没关系,但养成strip()的习惯能避免后面处理字符串题目时踩坑。
3. 第8题:回文数判断的边界细节
3.1 题目内容与核心考点
第8题通常是回文数判断或类似数字反转问题。常见描述如下:
输入一个整数,判断它是否为回文数。回文数是指正序和倒序读都相同的整数,比如121、1331、5。如果是,输出Yes,否则输出No。
这类题有两个完全不同的解题路径:
- 将整数转成字符串,反转后与原字符串比较。
- 用数学方式把整数倒序反转,再与原数比较。
两种都能过,但出题人设置这道题,通常希望你先考虑第二种。因为字符串方式虽然直观,但需要处理负数、前导零等边界问题,而且OJ的内存和运行时间虽然不受影响,但作为基础题,你更应该在练习阶段理解“数字本身的结构”。
3.2 数字反转法的核心逻辑
数学方式反转一个整数的思路,就是把原数不断取末尾数字,然后拼成一个新数。比如输入12321:
- 第一次:取余得1,新数=1,原数=1232
- 第二次:取余得2,新数=12,原数=123
- 第三次:取余得3,新数=123,原数=12
- 第四次:取余得2,新数=1232,原数=1
- 第五次:取余得1,新数=12321,原数=0
最后发现新数等于原数,说明是回文数。
C语言参考写法:
#include <stdio.h> int main() { int n; while (scanf("%d", &n) != EOF) { int original = n; int reversed = 0; while (n > 0) { reversed = reversed * 10 + n % 10; n /= 10; } if (reversed == original) { printf("Yes\n"); } else { printf("No\n"); } } return 0; }3.3 容易被判错的三个边界
第一,负数。像-121这种数字,倒过来是121-,显然不是回文数。上面这个写法里,while (n > 0)直接不会进入循环,reversed保持0,和original(-121)不相等,输出No。这其实是正确的,不需要额外处理。
第二,个位数。5反转后还是5,是回文数。上面的写法能正确处理,因为n > 0条件只执行一次,reversed就是5。
第三,多组读入时reversed变量是否清零。这是一个非常容易踩的坑。如果每次循环里,你忘记给reversed重新赋值为0,那么第二组数据计算时,reversed会带着上一轮的旧值,结果必然出错。这个属于“变量初始化”老生常谈,但在OJ题里反复出现,值得单独提醒。
3.4 字符串做法和它的问题
用字符串做法也可以,而且代码更短。以Python为例:
n = input().strip() if n == n[::-1]: print("Yes") else: print("No")这个写法在C语言里就是转字符数组,然后头尾指针比较。问题在于:如果题目输入的是-121,字符串反转是121-,自然不等,所以也不用特殊处理。看起来字符串做法更简单,那为什么还要学数字反转?
因为后面的题目里,很多场景会让你“反转一个整数后再参与数学运算”,比如“反转数字后求和”“判断反转数是否为素数”。到时候你已经拿到的是int,再转字符串反而绕远路。基础第8题的意义,就是让你趁早把“整数的位拆分与拼接”练熟。
3.5 一个值得注意的溢出场景
数字反转法在两个测试数据非常大的题目里,可能会碰到一个隐含问题:如果反转后的数字超出int范围,C语言的int会溢出,变成不可预测的结果。但基础题通常不会给到这么大范围。
如果题目有范围说明,比如“整数在32位有符号整数范围内”,那你要判断反转结果是否会越界。一种防御式写法是:在拼接时先判断reversed > (INT_MAX - n % 10) / 10,如果成立,直接返回0(表示不是回文数)。但这类细节在基础7-9题里一般不需要,基础阶段你先跑通标准情况,等后面做题遇到再补上这个防御。
4. 第9题:数组排序与双重循环输出
4.1 题目内容与核心考点
第9题常见形态有两种:一种是“输入n个数字,按升序输出”,另一种是“输出九九乘法表”。两种都属于“循环控制”的经典题型。这里我以排序为例讲透,因为排序题引入的“元素交换”和“双层循环”是后面几乎所有复杂算法的地基。
输入一个整数n,接着输入n个整数,将这n个整数从小到大排序后输出,数字之间用空格分隔。
这道题你当然可以直接调用库函数排序,比如C语言的qsort、Python的sorted(),都能通过。但OJ把这道题放在基础第9题的位置,其真实意图是让你手动实现一次冒泡排序或选择排序。
4.2 朴素选择排序的推导过程
选择排序的思路和生活里整理书架上很像:你从一堆书里找到最小的一本放到最左边,然后从剩下的书里再找最小的放到第二位,以此类推。
写成逻辑就是:
- 从第0个位置到第n-2个位置,逐个确定每个位置上的最小值
- 确定方式是:用一个内层循环,在当前下标之后的所有元素中找到最小的那个,然后交换到当前位置
C语言参考写法:
#include <stdio.h> int main() { int n; while (scanf("%d", &n) != EOF) { int a[1000]; for (int i = 0; i < n; i++) { scanf("%d", &a[i]); } for (int i = 0; i < n - 1; i++) { int minIndex = i; for (int j = i + 1; j < n; j++) { if (a[j] < a[minIndex]) { minIndex = j; } } if (minIndex != i) { int temp = a[i]; a[i] = a[minIndex]; a[minIndex] = temp; } } for (int i = 0; i < n; i++) { if (i > 0) { printf(" "); } printf("%d", a[i]); } printf("\n"); } return 0; }4.3 为什么输出时空格的判断要这样写
你看上面输出部分,我用的是if (i > 0) printf(" ");,而不是在每个数字后面加空格。区别是什么?
如果写成这样:
for (int i = 0; i < n; i++) { printf("%d ", a[i]); }最后会多出一个空格。OJ判定输出时,多一个空格通常会判Presentation Error,虽然不算Wrong Answer,但依然算你没过。所以“行末无多余空格”是OJ非常基础的素养。用这个技巧,可以同时处理“第一个数字前没有空格,中间严格一个空格,末尾没有空格”的要求。
4.4 冒泡排序的替代写法
另一个常见写法是冒泡排序。也不少同学选它,因为结构对称、好记。核心逻辑是从第一个元素开始,相邻两两比较,把大的往后移。每次内层循环结束,会把当前未排序部分的最大值送到末尾。
for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (a[j] > a[j + 1]) { int temp = a[j]; a[j] = a[j + 1]; a[j + 1] = temp; } } }注意内层循环的边界是j < n - 1 - i,不是j < n - 1。因为每一轮都会让最大值沉底,沉底的那些位置已经排好了,不需要再参与比较。如果你写成j < n - 1,程序也能运行,但会多做很多无效比较。在基础OJ里或许不超时,但这是一个不太好的习惯。
4.5 如果第9题是九九乘法表
有些OJ把第9题出成“输出九九乘法表”。这种题考察的核心是双层循环的嵌套关系:
外层循环控制行(1到9),内层循环控制列(1到当前行数)。关键坑是内层循环的终止条件要随外层变化,不能写死。输出时每个算式后面跟几个空格,以及什么时候换行,需要根据题目样例去对齐,一般用%d*%d=%2d这种格式控制。
很多人的乘法表输出对不齐,就是因为%2d和题目的要求不一致。这种问题没有捷径,只能对着样例,一个字符一个字符对比。
5. 提交与评测:那些“本地对、OJ错”的典型现场
5.1 多组输入怎么判断什么时候结束
前面提到,OJ基础题普遍是多组输入。判断结束的方式在C语言里就是scanf返回EOF。新手最常见的错误是只写了一个scanf,然后只处理一组数据。
正确的处理姿势是:
while (scanf("%d %d", &a, &b) != EOF) { // 处理逻辑 }如果题目输入是一行行读入,写法可能是:
while (scanf("%d", &n) == 1) { for (int i = 0; i < n; i++) { scanf("%d", &a[i]); } }第二行的scanf("%d", &a[i])不需要再判EOF,因为题目已经通过第一个n告诉你要读几个数。这两个写法的场景完全不同,要分清楚。
5.2 Presentation Error和Wrong Answer的区别
OJ的常见错误码里,最迷惑人的就是PE(Presentation Error,输出格式错误)和WA(Wrong Answer,答案错误)。PE通常意味着你的计算结果没问题,但“长相”不对。比如该换行没换行、数字之间多了空格、输出末尾多了一个空行。
WA则意味着计算结果本身和标准答案不一致。遇到WA,先检查数据读取逻辑,再看算法边界。用最小数据和最大数据各测一遍,往往能暴露问题。
5.3 本地测试要测哪几组数据
我建议每一道OJ题,不管多简单,提交之前至少测三组:
- 题目给的样例数据
- 最小边界数据(比如只有1个数字、数字为0或1的情况)
- 较大的数据(比如两个大整数、n取最大值的情况)
每次我帮新生排查OJ题,先问的就是“你测过边界数据吗”。得到的回答十有八九是“我只测了样例”。不是样例没用,而是样例通常只覆盖常规情况,回文数题目里的个位数、公约数题目里的相等数,样例很少覆盖到。
5.4 数组声明的大小要留足余量
排序题里,数组声明经常会变成定时炸弹。如果你声明int a[100];,而题目输入数据量能到10000,程序就会越界,轻则本地跑出奇怪结果,重则直接段错误。我的习惯是:如果题目没有明确说数量级,就把数组开到100000以上;如果有明确上限,就比上限多10个。
这样做的原因很简单:OJ评测对内存的限制通常很宽裕,你多声明一点内存不会额外扣分,但数组越界带来的问题非常难排查。
6. 三道题的避坑经验与排查思路实录
6.1 变量初始化:多组输入下的隐形杀手
我在带人刷OJ基础题时,见过的最多错误就是“变量没清零”。比如回文数题里,每次循环开始前,reversed必须重新初始化为0。如果把它定义在while外,而且每次循环不重置,第二组数据就会把第一组算出来的结果带进去。
C语言中一个非常推荐的写法是:在用到的变量时再声明,把作用域限制在每次循环内部。
while (scanf("%d", &n) != EOF) { int reversed = 0; int original = n; ... }这样每次迭代都会新建变量,天然规避了“上一次残留”的问题。用C99或更新的标准,编译器支持在循环体内部声明变量,这没问题。
6.2 判断是否整除时的数据类型陷阱
最大公约数题目里,很多人会顺手写出:
long long l = (long long)a * b / g;这里如果把(long long)去掉,a * b按int计算就可能溢出。关键是,不少人觉得“我明明定义了long long l,为什么还溢出”。因为溢出发生在a * b这一瞬间,是在赋值给l之前。解决方案就是先做一次类型转换,或者像我前面写的,先除后乘。
6.3 如果一直WA,试试把思路换成另一个方法
第8题如果一直WA,而你觉得思路没问题,可以试试换一种实现方式。比如你用的是字符串反转,那改用数字反转;你用的是数字反转,那改成字符串反转。同类型的题,两种方法互相验证,能帮你快速定位问题是出在“算法思路”还是“代码细节”。
我在排查一次代码时发现,学生用数字反转判断回文数,思路完全正确,但把n % 10写成了n / 10,导致结果完全错乱。这种情况下,换一种写法反而更容易发现低级笔误。
6.4 参考代码可以看,但别直接抄
很多新生刷OJ喜欢直接搜“东华OJ基础7-9题题解”,复制粘贴跑出一份AC就完事。我当然也理解这种心情,但这样做对这个阶段来说损失很大。因为基础题考察的就是那几个小知识点,你自己参考着写一遍,跟直接抄一遍,收获差距非常大。
我的建议是:把参考代码当作“参考答案”,自己先在纸上写出处理流程,再动手写代码。如果卡住了,再看参考代码里对应的那一段,而不是整段拷贝。
常见问题速查表
| 错误提示 | 可能原因 | 排查方法 |
|---|---|---|
| Wrong Answer | 多组输入没处理,只跑了一次 | 改用while(scanf(...) != EOF) |
| Wrong Answer | int类型溢出,a*b中间值超范围 | 用long long,或a/g*b |
| Wrong Answer | 变量未清零,第二组数据残留 | 检查变量初始化位置 |
| Presentation Error | 行末多空格、多空行 | 用if(i>0)控制空格,末尾一个换行 |
| Compile Error | scanf里忘了写&符号 | 检查所有scanf的参数 |
| Runtime Error | 数组越界,输入数量超过数组大小 | 数组开大一点,加入边界判断 |
| Time Limit Exceeded | 死循环,或没有正确判断EOF | 检查输入终止条件和循环退出条件 |
这个表我建议你保存下来。后面刷任何OJ题,遇到类似错误码,优先按这个思路排查。
一点个人经验
我刷题和带新人的次数多了之后,最大的体会是:OJ基础题7-9其实不在于“会不会”,而在于“稳不稳”。很多人一遍就AC,不是因为他们聪明,而是因为他们对输入输出格式、变量初始化这些细节形成了肌肉记忆。
每次提交之前,我都习惯花30秒再过一遍:题目要求几个输入、几个输出、每个一行还是同一行用空格隔开、测试数据最大能到多少。这30秒能帮你省下好几次无效提交。这道题的后面,其实还有很多变体可以继续练,比如“最大公约数加上递归写法”“回文数加上字符串专题”“排序加上结构体二级排序”,每一条延伸出去都能衔接后面的进阶题。