☰
DHUOJ基础题40、41、42拆解:从取余到数组统计的OJ刷题关键
2026/10/14 4:25:22 网站建设 项目流程

老实说,我第一次在 DHUOJ 上看到“基础 40 41 42”这个题组排列的时候,正蹲在机房帮同学排查一份反复 WA 的代码。当时的感受就是:这几个题号看起来平平无奇,但它基本决定了你后面在 OJ 上做题的“手感和运气”。后来我自己在线上判题系统上带过几次基础训练,也亲眼看过不少同学从这三题开始一路顺利,也有卡在第一题就心态崩掉的。这篇文章不打算写什么高深算法,就想把 40、41、42 这三道基础题从头到尾拆开,讲清楚每道题到底在考什么、代码该怎么写、提交时最容易在哪个环节翻车。无论你是刚接触 OJ 的新手,还是想帮别人讲题的老手,这篇内容都能直接拿去用。

这三题的难度排序非常典型:从顺序结构到循环,再到数组统计,每一步都踩在“基础语法刚学完”和“第一道算法题还没开始”的中间地带。看起来只是三行五行的代码量,但里面藏着的读入习惯、边界判断、浮点精度处理,几乎决定了你后续所有题目的通过率。这篇文章适合所有正在刷 DHUOJ、或者准备开始刷任何 OJ 平台基础题的人。

1. 项目背景:DHUOJ 基础题组到底在练什么

1.1 为什么 OJ 上的基础题最值得认真对待

很多人觉得基础题就是“送分题”,随便写写就能过,于是上来就敲代码,敲完提交,WA 了就改,改不过就弃。这个思路在入门阶段特别吃亏。DHUOJ 的基础题组设置的真正目的,不是让你练那几行代码,而是让你在提交评测的过程中形成一套“对答案的敬畏心”。

什么叫对答案的敬畏心?就是你得接受一个事实:OJ 的评测只看输出结果,不看过程。你的代码在本机跑再多遍都只能证明“我觉得没问题”,提交上去以后,评测程序会用你完全没想到的测试数据去撞你的程序。哪怕只有一个数字算错、一个换行位置不对,结果就是 WA 或 PE,没有任何商量余地。基础 40、41、42 这三题,恰好把最常见的几种“我认为没问题”的情况全暴露了一遍。

我陪练的时候见过一个特别典型的同学,他在自己电脑上测试第 40 题,输入 123、246、999,输出全对,就提交了。结果 WA。他一脸震惊地问我为什么,我把评测数据里的一组“100”丢给他看——他程序输出的是 1,但正确答案是 0 + 0 + 1 = 1。等下,100 的各位和确实是 1,这没错。那他为什么 WA?因为他代码写的是把三位数当成“百位 + 十位 + 个位”然后直接输出,但输入 100 的时候他的某个中间变量居然因为除法取整出了问题。这个例子之后我再细讲,这里我只想说:基础题不仔细,后面真没得玩。

1.2 “40、41、42”在基础训练链中的定位

不同学期、不同题库版本里的 DHUOJ 题号,对应的具体题面可能不完全一样。但 40、41、42 这个连号,在绝大多数训练计划里都遵循一条隐含的逻辑链:先练“把一个数学问题翻译成表达式”,再练“把重复动作交给循环”,最后练“把多个数据存起来做统计”。

用大白话描述就是:第 40 题教你怎么算;第 41 题教你怎么重复算;第 42 题教你怎么把一堆结果汇总起来算。这个递进关系非常重要。很多同学到后面写不出动态规划、写不出双指针,回头一看,根子就在“数组遍历 + 条件统计”这一步没真正想明白。如果你现在刚做到 40、41、42,完全不用着急往后刷,把这三题吃透,比盲目刷三十道简单题都有用。

2. 三道题的考察点拆解与解题思路

2.1 第 40 题:输入输出、取余与整数运算

我按最常见的题面展开:输入一个三位正整数,输出它的各位数字之和。比如输入 123,输出 6;输入 100,输出 1。题面简单到不能再简单,但里面藏着两个新手必踩的坑。

第一个坑是“整除和取余的优先级理解”。要把一个三位数拆成百位、十位、个位,标准做法是:百位用 n / 100,十位用 n / 10 % 10,个位用 n % 10。这个组合看起来像背公式,但如果不理解除法在整数类型里的“截断”行为,就会出现一种很隐蔽的错法:有人先把 n 转成浮点数再算,结果输出带了一堆小数点,直接 PE;有人把十位写成 n % 100 / 10,结果在 n 的个位为 0 时依然正确,但一旦涉及 10 的倍数就会乱掉。

第二个坑是“输入 100 这类边界数据”。如果代码里用了 n / 100,那 100 的百位是 1,没问题;但如果你图省事写了个循环去一位一位拆,循环结束条件写错,就会把 100 拆成“1、0、0”之外的东西。别笑,我见过有人用 while(n) 循环,结果在处理 n = 100 时把循环条件写成 n > 0 且内部用 n % 10 和 n /= 10,这样其实是正常的。真正翻车的是有人把循环写成 for(i = 0; i < 3; i++),但循环体里先执行了 n %= 10,于是第一次取到 0,后面两次全取 0,最终输出 0。这种错在本地测试时很难发现,因为你通常先测试 123 而不是 100。

所以第 40 题的解题思路核心就是:先明确数据类型,再用整数除法与取余把数位拆干净,最后老老实实求和。别耍小聪明去转字符串,虽然也能过,但练不到取余这个关键技能。

2.2 第 41 题:循环、素数判定与边界处理

第 40 题是一次性算术,第 41 题就开始上循环了。最常见的题面是:输入一个正整数 n,输出从 1 到 n(含)之间有多少个素数。或者变体是输出这些素数之和。不管是哪种,核心都是“怎么判断一个数是不是素数”。

素数判定的标准写法是:从 2 试除到 sqrt(x),如果都除不尽,就是素数。注意两点:第一,1 不是素数,这是所有编程比赛里必考的边界;第二,2 是素数,但它既不能被 2 整除这个规则套进去,因为 2 % 2 == 0,如果你不特判,就会把 2 排除掉。所以代码里的第一步基本固定:if (x < 2) return 0;。

这里想多说一句循环边界。很多人写 for(i = 2; i <= sqrt(x); i++),在本地能过,提交却 TLE。原因不是思路错,而是每次循环都调用 sqrt(),效率低。基础题的数据量一般不大,这么写通常也能过,但一旦 n 到十万、百万级别,就会发现慢得离谱。更推荐写成 i * i <= x,把乘法代替开方。当然,如果 x 比较大,i * i 可能溢出,这时候可以换成 long long,或者用 i <= x / i。第 41 题的数据范围通常用不上这么复杂,但习惯从这道题开始养起,后面会省很多事。

2.3 第 42 题:数组遍历、统计与浮点精度

第 42 题就进入数组了。最常见题面是:输入一个整数 n,然后输入 n 个学生成绩,先输出平均分(保留两位小数),再输出高于平均分的人数。

这题比 40、41 多了一层思维跳跃:你不能一边读一边统计“高于平均分的人数”,因为你得先知道平均分是多少。所以必须先把所有成绩存进数组,第一遍读入时累加总和,第二遍遍历数组时逐个和平均分比较。这个“两遍扫描”的思路,是后面几乎所有统计类题目的雏形。很多人第一反应是“那我读两遍输入不就行了”,在 OJ 上是不行的,输入数据不像文件一样能反复读。你必须用数组把数据存起来,这是第 42 题最核心的训练价值。

另一个大坑是浮点精度。平均分用 double 算,学生成绩是 int,比较时如果写成绩 > 平均分,C 语言会把成绩自动转成 double,这本身没问题。但如果你偷懒,把平均分直接定义成 int,比如总成绩 100、人数 3,平均分 33.33 却被存成 33,那高于平均分的人数就会全部算错。所以只要涉及“保留几位小数”或“和平均数比较”,平均分必须是浮点类型,而且比较时最好用 (double)scores[i] > avg,把类型转换写清楚,避免编译器隐式转换带来的意外。

3. 参考实现与关键代码讲解

3.1 第 40 题:完整 AC 代码与每一行在做什么

直接给一份我常用的参考实现,C 语言版:

#include <stdio.h> int main() { int n; scanf("%d", &n); int sum = 0; sum += n % 10; // 取个位 n /= 10; // 去掉个位,原来的十位变成新的个位 sum += n % 10; // 取新的个位,也就是原数的十位 n /= 10; // 再去掉一位,剩下百位 sum += n % 10; // 取百位 printf("%d\n", sum); return 0; }

这份代码的核心思路是:不把三位数拆成三个独立变量,而是用“不断除以 10 不断取余”的方式,把整个流程统一起来。每次 n /= 10 之后,原来的十位就跑到个位的位置上,再 % 10 就能拿到。这个技巧在题目扩展到“任意位数”时特别有用,比如输入一个整数求各位数字和,用循环写,而不是只会写 n / 100 这种固定写法。

有同学会问:为什么不用数组、不用字符串?因为这题考的就是整数运算,字符串会引入字符和数字之间的转换,属于杀鸡用牛刀,还可能因为忘记减 '0' 导致输出一堆 ASCII 码。基础训练阶段,老老实实用整数运算把数位拆明白,比什么奇技淫巧都划算。提交的时候注意输出格式,只要一个数字加一个换行,不要在中间加空格。

3.2 第 41 题:筛选法的引入时机

第 41 题先给一份最直接的实现,适合新手理解:

#include <stdio.h> int is_prime(int x) { if (x < 2) return 0; // 1 不是素数 for (int i = 2; i * i <= x; i++) { if (x % i == 0) { return 0; // 能被整除,不是素数 } } return 1; } int main() { int n; scanf("%d", &n); int count = 0; for (int i = 2; i <= n; i++) { if (is_prime(i)) { count++; } } printf("%d\n", count); return 0; }

如果题目要求的是“输出素数之和”,就把 count++ 改成 sum += i,最后输出 sum。这份代码的正确性很高,边界也处理得比较干净。我特意把 i * i <= x 写在循环条件里,就是为了告诉你:不要用 sqrt,这题的数据量虽然不大,但养成好习惯比什么都重要。

如果 n 的范围很大,比如 n = 1000000,这份代码就会超时。这时候就要引入埃氏筛法:先开一个 bool 数组标记素数,从 2 开始,把所有 2 的倍数筛掉,再找下一个没被筛掉的数,继续筛。这个算法是第 41 题之后最常见的升级题,但我不建议在第一遍学这道题时直接上筛法。原因很简单:你得先能徒手判断 10 个 20 个数里的素数,理解什么叫“试除”,再去学筛法,才知道筛法到底优化了什么。直接背筛法代码的人,多半过两天就忘了。

3.3 第 42 题:平均数比较的精度陷阱

第 42 题的参考实现,我写两个版本。第一个版本是用固定大小数组,适合 OJ 基础题:

#include <stdio.h> int main() { int n; scanf("%d", &n); int scores[105]; int sum = 0; for (int i = 0; i < n; i++) { scanf("%d", &scores[i]); sum += scores[i]; } double avg = (double)sum / n; int cnt = 0; for (int i = 0; i < n; i++) { if ((double)scores[i] > avg) { cnt++; } } printf("%.2f\n%d\n", avg, cnt); return 0; }

数组开 105 是因为题目通常 n <= 100,多开一点是 OJ 上的安全习惯。这里有个小细节:读入循环和统计循环是分开的,中间隔着一行 double avg 的计算。这个结构不是随便写的,它代表了“两遍扫描”的思维方式——第一遍拿数据,第二遍用数据。很多同学第一次写这题时,会在读入循环里顺手写 if (scores[i] > avg),但这个时候 avg 还没算出来,结果要么是随机值,要么是上一次循环残留的脏数据。这不是玄学,是代码执行顺序的问题。

第二个版本是如果 n 很大,用动态内存:

#include <stdio.h> #include <stdlib.h> int main() { int n; scanf("%d", &n); int *scores = (int *)malloc(n * sizeof(int)); // 后续逻辑一致,记得 free free(scores); return 0; }

但 DHUOJ 基础题基本用不上动态内存,固定数组就够。动态内存反而容易引入 malloc 失败、忘记 free 的问题。我在这里提它,是想让你知道:数组不是越大越好,也不是越小越好,而是要根据题目范围选择合适的容量。105 这个数字看起来随便,但它是“n <= 100 时最省心的开法”。

4. 提交评测的常见问题与排查实录

4.1 WA 最常见的三个原因:读入、初始化、边界

如果说 40、41、42 这三题有一个共同的敌人,那就是 WA。我帮人排查过的 WA 代码里,出现频率最高的三个原因如下。

第一,scanf 忘记取地址。写 scanf("%d", n) 而不是 scanf("%d", &n),这在 C 语言新手阶段几乎人人犯过。症状非常典型:本地编译可能报 warning,也可能不报;运行的时候输入一个数,程序直接崩溃或者输出一个巨大无比的数字。解决方式很简单,看到 & 丢了就补上。但为了让你不再犯,建议你多写几遍,写到形成肌肉记忆。

第二,累加变量没初始化。定义 int sum; 之后直接 sum += n % 10;,sum 的初始值是随机的,输出自然也是随机的。这个问题在 40 题和 42 题都会出现。我见过有人在 42 题里没初始化 sum,导致平均分算出来是一个天文数字,然后高于平均分的人数变成 0,他还以为是题目数据有问题。其实只要写 int sum = 0;,一切就好了。为什么必须初始化?因为 C 语言不会自动帮你把局部变量清零,这是很多现代语言不会踩的坑。

第三,边界判断没写全。第 41 题里 1 不是素数,第 42 题里 n 可能等于 0(虽然大多数题面不会这么出)。如果你默认所有输入都是合法且充足的,那你一定会在某个隐藏测试点上翻车。正确做法是:写代码之前先想一遍“这个数最小是多少,最大是多少,中间有没有特殊值”,把边界都列出来,再动手写。这个习惯比任何语法知识都值钱。

4.2 TLE、PE、RE 的排查思路

TLE 在第 41 题里容易出现。如果你看到别人用埃氏筛法,而你用的是双重循环暴力试除,n 稍微大一点就超时。排查思路很简单:先把 n 的范围打印出来,如果 n <= 1000,那份代码不优化也不会 TLE;如果 n <= 1000000,那就必须考虑筛法或者降低复杂度的写法。还有一个常见低级错误是死循环,比如 while 循环结束条件写反,n 永远减不到 0,程序就永远跑不完。这时候评测结果也会显示 TLE。遇到 TLE,先别急着优化算法,先确认自己的循环是不是真的能结束。

PE 是 Presentation Error,翻译过来是“输出格式不对”。最常见的是多了空格、少了换行、或者中间多了逗号。第 42 题要求先输出平均分,再换行输出人数,如果你把两个输出写在同一行,或者平均分后面少了换行,就会 PE。这个错误最扎心,因为你的答案全对,只是因为格式问题不给你 AC。排查方法:把题目要求的输出样例逐字节比对,空格和换行都要看。第 40 题的 PE 通常来自多打了一个空格,比如 printf("%d ", sum) 和 printf("%d\n", sum) 看似差不多,OJ 上就是天壤之别。

RE 是运行时错误,常见于数组越界和除数为 0。第 42 题特别容易出现:n 输入为 0 时,double avg = (double)sum / n 就会除以 0,程序直接崩。虽然大多数题面会保证 n > 0,但如果你把代码提交到一个更严格的训练题库里,这种隐藏数据就可能出现。保险做法是输入后加一句 if (n <= 0) return 0;,或者直接设定题目保证 n 为正整数。RE 的另一来源是数组下标为负数,比如 scores[-1],这通常发生在循环边界写错时。

4.3 从 40、41、42 延伸出的巩固建议

刷完这三题之后,我不建议你立刻冲向难题,而是做几件能把地基打得更牢的事。

第一件是“一题多写”。比如第 40 题,你除了用三步取余,还可以用循环写一遍、用字符串写一遍。不是为了炫技,而是为了对比不同写法的适用场景。等你哪天遇到“不知道输入有多少位”的题,就会感谢当年写过循环版。

第二件是“改题”。把第 41 题的“输出素数个数”改成“输出第 n 个素数”,或者把第 42 题的“高于平均分的人数”改成“低于平均分且不低于及格线的个数”。改一次题,你的思路就会被逼着往前走一步。老话说熟能生巧,在 OJ 上最好的熟练方式不是重复刷题,而是自己给自己出变体。

第三件是“整理错误笔记”。我到现在还留着当年在 OJ 上 WA 的记录,每一条都写着原因:这个是因为 scanf 忘了 &,那个是因为整型除法的截断理解错了。等你攒到二十条以上,你会发现常见错误翻来覆去就那么几个。这份笔记比任何算法模板都好用,因为它是从你自己的真实错误里长出来的。

我自己带训练的时候经常说一句话:基础题不丢人,丢人的是基础题错了十遍还不知道为什么错。DHUOJ 的 40、41、42 正好提供了三个最典型的错误场景,值得你多折腾几次。如果你把这三题背后的“读入、取余、循环边界、数组两遍扫描、浮点比较”全部消化干净,后面遇到再难的题,你至少不会在起步阶段输得不明不白。

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

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

立即咨询