东华OJ基础题69到73这一连续区间,在学弟学妹群里被问到的频率一直不低。理由很简单:这五道题几乎是东华大一C语言课“从语法到算法”的分水岭,前面的题目主要考你“知不知道这个语法”,到这里开始考你“能不能把语法组合起来解决问题”。不少人前面刷得飞快,到这一组突然卡住,一卡就是好几天。我当年也是从这道坎跨过来的,所以想把这一组题目的底层逻辑、常见写法还有我反复踩过的坑一次性说清楚,给正在刷题的你省点时间。
先明确这套题适合谁:正在学C语言、刚接触OJ判题系统的大一学生,或者自学编程想找一套“有梯度、不劝退”的入门题单的人。它能帮你解决的问题很具体——怎么把循环、数组、字符串、枚举这些零散知识点组合起来,写成能通过Judge的代码。它是一块很典型的“承上启下”路标,刷透这一组,你后面接触递归、排序、结构体时会顺很多。
1. 从题目编号看东华OJ的出题规律
1.1 69到73这五题在题库中的定位
东华OJ的基础题部分,编排逻辑其实挺清晰的:前60题基本围绕分支、单层循环、简单数学公式展开,一题只考一个点。从60多题开始,题目开始“混合”知识点,比如循环里面嵌分支、数组里面做统计、字符串和循环结合处理。69到73正好处在这个过渡区的最深处,所以你会明显感觉到——一看题目都懂,一写代码就错,错完还很难查。
这五题具体考什么,我根据刷题记录和带新人的经验做一个大致归类(不同年份题单可能有微调):数字分解与重组、字符统计与判断、数组筛选与极值、递推数列、简单过程模拟。这些恰恰是后续一切算法题的“基本动作”。比如数字分解是数位DP的雏形,递推数列是动态规划的雏形,过程模拟则是贪心、搜索类题目的基本功。你在这一组花的时间,后面都会连本带利赚回来。
1.2 为什么基础题比难题更值得反复做
很多同学喜欢一上来就怼难题,觉得基础题“没技术含量”。但以我带人刷OJ的经验,基础题的价值恰恰在于它的约束少、出错空间大。难题往往框架固定,你知道要用排序、要用搜索,代码结构摆在那边;基础题反而容易暴露细节问题——循环变量初始化位置写错了、数组越界一位、scanf里忘了加&、多组输入没清空缓存,这些问题在难题里会被复杂逻辑掩盖,在基础题里却会赤裸裸地让你WA到怀疑人生。
还有一点,OJ基础题教会你的并不是“怎么做对一道题”,而是“怎么稳定地对很多道题”。这种稳定性靠的是反复训练形成的肌肉记忆,而不是临时想出来的灵光一现。把69到73这一组题刷透、刷出感觉,你后面再看任何题目,第一反应都会从“这题怎么下手”变成“这题属于我熟悉的哪一类”,这种思维转变才是做题量积累的真正回报。
2. 核心细节解析与实操要点
2.1 输入输出格式的规范是第一只拦路虎
OJ题和普通练习题最大的区别就是:判题程序只认你程序的输出结果,不多不少、不差一个空格。很多人代码逻辑完全正确,但就是因为输出格式差一个空格被判定WA,这种亏我吃过不止一次。69到73这一组的题目大多是单组输入,但也会有那种“输入多行数据,直到遇到特殊标记才结束”的题目,你要做的第一步永远是仔细读题,搞清楚输入到底是一行、多行还是不定长。
拿数字分解这类题来说,如果题目要求“输出每位数字,用空格分隔”,那么最后一个数字后面绝对不能有空格。我见过不少同学用printf("%d ", digit)这种写法一路打到底,最后被判WA还百思不得其解。规范做法是:要么先输出第一个数字,后面再输出前缀空格;要么把数字存进数组,最后统一输出。这类细节,只有经历过一次WA才会长记性,但你能从别人经验里学会,就没必要自己撞墙了。
// 输出每位数字,空格分隔,末尾无空格 int digits[10]; int cnt = 0; while (n > 0) { digits[cnt++] = n % 10; n /= 10; } // 现在digits里是逆序的,需要倒着输出 for (int i = cnt - 1; i >= 0; i--) { if (i < cnt - 1) printf(" "); printf("%d", digits[i]); } printf("\n");2.2 数据类型选不对,算得再对也白搭
基础题里最容易忽略的问题是数据范围。很多人一看“输入一个整数”,下意识就用int,完全没注意到题目可能写的是“输入一个正整数(不超过10^9)”或者“输入浮点数”。10^9以内int还扛得住,但一旦涉及累加、累乘,结果很容易爆掉。69到73这组题里如果有数列递推、求和之类的题目,中间结果大概率会超过int上限。
我举个实际例子:计算斐波那契数列第45项,int直接溢出变成负数,你还以为是算法写错了,排查半天。这种坑非常隐蔽,因为前几项都是对的,到后面突然开始输出乱七八糟的数。所以拿到题目先看数据范围,再看运算过程,如果中间结果可能超过2^31-1,就用long long,如果涉及乘法且结果可能超过2^63-1,就得考虑换思路或者用其他技巧了。
2.3 循环边界与循环变量是出错的重灾区
循环边界写错,在69到73这组题里几乎是人人都犯过的毛病。常见的错法有三种:应该循环n次写成n-1次;数组下标从0开始计数但循环边界用1到n导致越界;还有while循环里的条件写反,一进入循环就无限循环或者直接跳过。这些问题在编译器层面不会报错,只能靠你人肉debug。
调这类问题我有个土办法:在循环体里加一个printf打印当前循环次数和关键变量,跑一次看输出就知道边界问题出在哪了。排查完记得删printf,不然会因为多余输出被判PE或者WA。循环边界这个问题的本质,是你对“循环不变量”的理解不够清晰——你在循环开始前、循环中、循环结束后,期望哪些变量是什么状态,需要心里有数,这部分如果没想明白,写出来的边界必然是飘的。
3. 实操过程与核心环节实现
3.1 数字类题目的通用拆解法
东华69到73这一组里,数字处理类题目出现的频率不低,比如给一个正整数,让你把它的每一位拆出来做运算。这类题的核心就一句话:对10取余拿到最低位,整除10去掉最低位,循环到数变成0为止。这个“拆数”模板你只要掌握了,什么求位数、求各位和、逆序输出、判断回文数,全是套同一个框架的事。
// 拆数模板:取出正整数的每一位 // 以 n = 12345 为例,运行过程如下: // 第1轮:digit=5, n变成1234 // 第2轮:digit=4, n变成123 // 第3轮:digit=3, n变成12 // 第4轮:digit=2, n变成1 // 第5轮:digit=1, n变成0,循环结束 int n = 12345; while (n > 0) { int digit = n % 10; // 对digit做你想做的处理 n /= 10; }注意一个问题:这个模板对“n=0”的情况是失效的,因为循环直接不执行。如果题目允许输入为0,你得单独判断。我在这个细节上栽过跟头,题目说输入一个非负整数,我默认它大于0,结果0这个边界值让我直接WA。后来长记性了,凡是“非负”这个字眼出现,我一定要单独处理0的情况。
3.2 字符统计与字符串处理的核心写法
字符类的题目在69到73这组里也常出现,比如统计一段字符串里数字、字母、空格各有多少个,或者判断某个字符在字符串里出现的次数。C语言处理字符串和数组的区别,很多新手容易绕进去——数组长度是固定的,字符串却是以\0结尾的,你遍历字符串的时候可能把\0也当成普通字符处理了。
这里有一个关键认知:在C语言里,char类型的本质其实就是整数,你可以直接拿字符做比较运算,比如c >= '0' && c <= '9'判断数字字符,c >= 'a' && c <= 'z'判断小写字母。英文字母的判断还有一种写法,直接用<ctype.h>头文件里的isdigit(c)、isalpha(c),更简洁也更不容易写错,但前提是你记得引入对应头文件。
// 统计字符串中的数字、字母数量 #include <ctype.h> char str[1000]; // 假设str已经从键盘读入 int digit_count = 0; int letter_count = 0; for (int i = 0; str[i] != '\0'; i++) { if (isdigit(str[i])) digit_count++; if (isalpha(str[i])) letter_count++; }3.3 数组筛选与最值查找的套路
“输入n个数,找出最大值/最小值/所有素数/所有大于平均数的数”——这一类题目在OJ基础题中属于必考题型,69到73区间应该也不会缺席。它们考察的核心能力是做“筛选”,就是遍历一遍数组的同时,把符合某个条件的元素摘出来。这看起来简单,但有一个老生常谈的错误:找最大值时,max变量的初始值应该设为数组的第一个元素,而不是拍脑袋设成0。
为什么不能设成0?因为如果数组里全是负数,max=0会导致你永远选不出真正的最大值。正确的初始化方式有两种:一是int max = a[0];,先让max等于第一项,然后从第2项开始比较;二是把max初始化为一个非常小的数,比如INT_MIN(需要包含<limits.h>头文件)。第二种方式更通用,因为它不依赖于数组是否为空,我个人更推荐。
另一个常被忽略的细节是“位置”和“值”的关系。很多题目不仅要你输出最大值,还要你输出最大值所在的位置(下标)。这时候你就要在更新max的同时,同步更新记录位置的变量。两个变量一起维护,这种思想后面学排序、学查找时也一直会用到。
3.4 递推数列的实现与边界处理
递推数列也是69到73这段题目里的常客,比如斐波那契数列、等差数列的变体、或者自定义递推规则。递推的灵魂在于“状态转移”——后面的结果依赖前面的结果,你用数组把每一次的结果存下来,后面直接用,而不是每次从头算。这看起来很容易理解,但新手常犯的错误是数组下标越界,比如第n项你存到f[n],但数组开的大小只到n,一访问f[n]就越界了。
数组开多大的经验法则是:题目如果问你前n项,你就开n+1的大小(甚至再大一点点留余量),下标直接对应项数,相比从0开始计数能少很多心智负担。另外注意递推的起点,比如斐波那契的第一项、第二项分别赋什么值,一定要看题目定义——有的题说第1项是0、第2项是1,有的说第1项和第2项都是1,这个没看清,整个数列全错。
// 计算斐波那契数列前n项 // 假设: f[1] = 1, f[2] = 1, f[k] = f[k-1] + f[k-2] (k >= 3) int f[100] = {0}; // 全部初始化为0,保证未赋值的元素不产生随机值 f[1] = f[2] = 1; for (int i = 3; i <= n; i++) { f[i] = f[i-1] + f[i-2]; }3.5 经典题型的完整代码实现参考
到这里,我完整写一道“读入正整数n,输出它每一位数字的和”的代码,把上面提到的拆数模板和边界处理串起来。这个函数在69到73那组题里经常作为某个小题的形式出现,弄明白它,你会发现在这一区间的很多题目都有它的影子。
#include <stdio.h> int main() { int n; scanf("%d", &n); // 特殊处理 n == 0 的情况 if (n == 0) { printf("0\n"); return 0; } int sum = 0; while (n > 0) { sum += n % 10; // 取出最低位,累加到sum里 n /= 10; // 去掉最低位 } printf("%d\n", sum); return 0; }这段代码的逻辑非常直观:每次循环取n的最低位累加,然后n缩小10倍。如果你已经想通了拆数模板,还试着思考一个变体——输入正整数n,计算出它总共有多少位。思路几乎一样,只是每次循环里做count++而不是累加。能自己推导出这个变体的写法,说明你真正理解了循环和取模运算的本质,而不是背模板。
4. 常见问题与排查技巧实录
4.1 如何系统性地排查WA(错误答案)
WA是最磨人的,因为它不像编译错误那样直接告诉你哪一行有问题。综合我这几年刷题和帮别人debug的经验,WA的排查应该按一个固定的顺序来,而不是东改一行西改一行碰运气。
第一步,检查是不是多组输入的问题。很多基础题其实会有多组测试数据,如果你只处理了一组就结束,第二组数据直接读不到了,结果几乎必WA。如果你不确定,就写个while (scanf("%d", &n) != EOF),这个写法能兼容单组和多组的情况,算是万能解法。第二步,检查边界值。题目有没有说n范围?n=1、n=0、n=10^9,你的代码分别输出什么?手动在本地跑这几个值,很多问题立刻现出原形。第三步,检查中间结果是否会溢出。把关键变量改成long long试试,如果AC了,说明是溢出问题。第四步,检查输出格式。空格、换行、回车、大小写,任何一个对不上就是WA。
这个排查顺序看起来简单,真遇到问题时能救你很多时间。最忌讳的就是“看到WA就随便改一个地方交一次”,这样做事的效率极低,而且会把原本正确的逻辑改坏。我当年也是交了不少“学费”才学会按顺序排查的,现在直接把这个经验给你,希望你少走弯路。
4.2 多组输入的正确打开方式
多组输入这个坎,几乎每个刷OJ的人都要踩一踩。东华69到73这组题目里,万一遇到“每次输入一行,处理一行,直到文件结束”的题,你要理解的核心概念是:scanf的返回值代表成功读入的参数个数。当它返回EOF(也就是-1)时,说明输入流已经结束了。所以while (scanf("%d", &n) != EOF)这个写法,本质上是在说“只要还能读到整数,就继续处理”。
读字符串的多组输入略有不同,while (gets(str))这种写法在C11标准里其实已经不建议用了,因为gets无法限制读入长度,非常容易缓冲区溢出。建议用fgets(str, sizeof(str), stdin),它能限定最大读取长度。如果你要读的是单个字符,还得考虑上一次输入后遗留在缓冲区的换行符,通常用getchar()先吞掉那个换行。
4.3 常见错误的速查对照表
| 错误类型 | 典型表现 | 排查方向 |
|---|---|---|
| 输出格式错误 | 本地运行结果正确,交上去WA | 检查末尾是否多了空格、少了换行,大小写是否一致 |
| 数据溢出 | 小数据对,大数据错 | 把int换成long long,检查中间运算是否爆范围 |
| 循环边界错误 | 结果整体对但个别项不对 | 打印循环变量,确认循环次数和区间开闭 |
| 数组越界 | 输出结果极其离谱 | 检查数组下标范围,确认开的大小是否够用 |
| 多组输入漏读 | 只处理了第一组数据 | 换成while(scanf(...)!=EOF)写法 |
4.4 我常用的调试三板斧
第一招是“打印中间量”。在关键步骤后加printf,把变量值打到屏幕上,跑完看一遍基本就定位了。查完删掉重新提交,不要忘。第二招是“构造边界数据”。比如n=1、n=2、n=100000这种极值,每个都跑一遍。正常数据可能掩盖边界问题,边界数据几乎一定是问题的引爆点。第三招是“小样本手算对拍”。随便挑一个小输入,自己在草稿纸上算一遍期望输出,再和程序输出对比。这招虽然原始,但定位逻辑问题比盯着代码发呆快一万倍。
这三招用熟了,你的debug时间平均能缩短一半以上。更关键的是,你会发现代码写的时候就越发小心了,因为你开始预判哪些地方容易出错,干脆从一开始就绕开那些写法。
5. 实操心得与后续扩展建议
东华OJ 69到73这几道题,放到整个编程学习的时间线上来看,其实是一个不起眼的小节点,但它的价值被很多人低估了。刷这一组题,表面上学的是“怎么做题”,实际上学的是一种思维模式——拿到一个具体问题,先拆解它的输入输出、边界条件、数据规模,再选择合适的基本操作组合起来解决它。这种拆解能力,才是编程和解题的核心素养,你以后写任何项目、做任何开发,都会用到这一套思维。
我的个人建议是不要满足于“AC了就过”,把这几题AC了之后再回头想想:我用的方法还有没有更好的替代方案?如果数据量扩大十倍,我的代码还能跑吗?如果把题目里的某个条件改一改,我的思路还成立吗?这些问题随便想一个,收获都比草草刷完十道题更大。
再往后,你可以把目光投向东华OJ后面更进阶的题目:开始出现结构体、指针、文件操作、简单排序算法、递归回溯。69到73这组题里的数组操作、循环控制、边界判断,都会在后面反复出现。到那时候你回头看,就会明白为什么我说“这五题值得认真对待”了。刷题是马拉松,这一组题是你不会掉速的补给站,慢一点不要紧,关键是把每一步踩实。