
东华OJOnline Judge平台上的基础题是很多刚开始接触ACM竞赛或者刚刚学完C语言的同学都要经历的一道门槛。我自己刷题的时候也在这里花过不少时间。说实话基础题虽然难度不高但包含的编程思维并不少很多后面写综合项目时会用到的习惯都在这会儿打下的底子。这次就结合我在东华OJ上刷16-18这三道题的过程聊聊题目背后的思路、容易踩的坑以及一些通用的解题套路。我一直觉得刷题不能只对答案要知道这道题在考什么用什么逻辑去拆解它就算换一道类似的题也能独立写出来这才是真的学会了。1. 东华OJ基础题的特点以及为什么值得死磕说实话我见过不少同学一上来就刷LeetCode中等难度结果被吊打之后又回头来刷OJ基础题。在这里我想说一个自己的观点基础题从来不是“浪费时间”它是在帮你建立编译思维和调试肌肉记忆。1.1 判题系统到底怎么评判你的代码很多刚开始接触ACM形式的编程题的人会对Online Judge的判题机制很懵。东华OJ和其他OJ一样你提交代码之后系统会用一组测试数据可能会有很多组去运行你的程序然后比对你的输出和标准答案是不是完全一致。这跟平时自己写个程序看到运行结果就完事完全不同一点点空白字符的差异都可能导致Wrong Answer答案错误。我个人把它理解成一次“逐字核对”的考试不是“老师看思路给分”而是“机器只认字面产出”。所以你在本地跑通过了不算数格式也至关重要。这里要养成的一个习惯就是——输出语句里的换行、空格必须严格对照题目描述。比如说要求输出“sum36”那你就不能输出“sum 36”多了空格或者“36”少了前缀。1.2 这三道题在全套基础题中的位置之前有不少同学问我“东华OJ基础16-18题到底难不难”通常我的回答是“难度中等偏下但很有代表性。”。这几道题刚好分布在基础题序列的前中期意味着前面几道题已经帮你掌握了基本的输入输出、循环和简单选择结构而16题开始就逐渐加入了一些需要你用算法思路去分析而不是靠直觉硬写的东西了。通常到这个阶段会涉及三种常见的能力测试方向数组的存储与遍历基本数学逻辑与找规律字符串或字符数组的简单处理。如果你发现这几道题开始吃力其实不用慌。它更像一个节点提示你该系统地梳理一下程序流程而不是继续在“照着课本敲例题”的阶段原地踏步。1.3 我建议的刷题节奏我当时给自己定的规则很简单每天只刷1-2道题绝不贪多但每道题必须吃透。所谓“吃透”我会分四个步骤自己独立思考20分钟以上就算毫无思路也不立刻看题解有了初步思路之后先在纸上画流程图或者写伪代码代码实现并反复提交直到Accepted通过为止通过后再去看别人的解法对比差距并总结。这样虽然慢但每道题都能留下长期记忆。尤其是16到18题这三位“老哥”我至今还能记得当初的一些实现细节就是因为这个过程足够扎实。2. 第16题从题目描述中提取核心数学模型的思路刷OJ题最重要的一步其实是读题。不少初学的同学题还没看完就急着写代码结果做的是另外一件事。第16题这类题目往往会给你一段看起来很长的描述其实核心就是一个非常简单的数学模型。2.1 题目中容易被忽略的细节以我当时做到的题为例具体的题目序号不同学校或不同批次可能略有差异大致是一个数据统计与输出类的题目它通常会给出多组输入每组输入包含一个整数n随后是n个整数。需要你计算并输出结果。这种题目有四个容易踩的坑多组输入题目没说“输入一次”那就是要处理到读到终止符号为止通常EOF也就是End Of File或者遇到某个特殊值。n的范围如果n最大能达到几千或几万你在设置数组大小时就要留有足够的余量不然运行时会越界。变量的初始值统计和累加的变量必须在每组数据处理前重新初始化否则会累加上一组的“历史遗留”。输出格式每个结果之间是不是有空行每个结果前面有没有前缀我当时由于没有注意到“多组输入”的问题第一次提交就超时了或者答案错误。后来养成了条件反射式的习惯看到题目中写“input contains multiple test cases”这类提示默认用while配合scanf的返回值做循环判断。2.2 数组遍历与状态标记的经典组合这类基础题经常需要你遍历一遍数组同时进行一些“状态标记”。什么是状态标记我来举个例子。比如它要你输出这组数据里的最大值和最小值。你可以先假设第一个元素就是最大值和最小值然后从第二个元素开始比较。这就是一种“标记”思想。又比如它要你找出一个数字是否出现过你可以用一个布尔数组作为标记位。16题的常见逻辑就是“遍历-判断-记录”三步走。写成代码往往很简单但这个思维链条的完整性很重要第一步读取数据存入数组第二步遍历数组逐项判断并记录结果第三步按照要求格式输出结果。不夸张地说这类题是把以后的算法竞赛基础压缩到了最直观的形式。我现在带新人时经常说“能不能把16题讲得头头是道基本就能看出你对数组和循环的熟练度。”2.3 我的参考写法与逐行分析下面给出一段我当时写的参考代码C语言我加了详细的注释方便你逐句对照#include stdio.h int main() { int n; // 使用EOF作为输入结束标志这是OJ题最常见的套路 // scanf的返回值是成功读取的变量个数如果等于1则继续 while (scanf(%d, n) ! EOF) { int arr[1000] {0}; // 假设n不超过1000根据题目范围调整 int max, min; int sum 0; for (int i 0; i n; i) { scanf(%d, arr[i]); // 依次读取n个整数 if (i 0) { max min arr[i]; // 先把第一个数赋给max和min } else { if (arr[i] max) max arr[i]; if (arr[i] min) min arr[i]; } sum arr[i]; } // 输出结果注意格式控制符 printf(max%d min%d sum%d\n, max, min, sum); } return 0; }这里有几处很关键的细节arr[1000] {0}确保数组初始化为0避免未知的垃圾值。max min arr[i]首次赋值时的处理方式很多人喜欢先把max和min初始化为0这在数据全为负数时会直接出错。printf里的格式字符串前后不可以有多余内容必须和题目示例一致。2.4 这类题还能怎么变着考千万不要以为16题考过就算了它会演化出多种“亲戚”。比如求平均值但要求保留两位小数求第二大的数统计正数、负数、零的个数把数组元素逆序输出。如果第16题你已经掌握了以上基础恭喜你这些变形无非就是“在遍历时多做点事情”核心逻辑没有任何变化。3. 第17题二维数组和矩阵思维代码规范从这里开始建立到第17题通常就会碰到跟矩阵相关的问题了。这里算是很多人的一个分水岭因为二维数组比一维数组抽象得多一旦涉及行列变化脑子就容易绕晕。3.1 东华OJ常见的矩阵操作考点记不太清楚当时做到的是矩阵加法、转置、还是对角线求和了但这些题型的本质是一样的搞清行列坐标的变化规律。我用一个比较有代表性的例子来展开——矩阵转置。矩阵转置的意思是将矩阵的行列互换。比如原来的第1行变成目标矩阵的第1列以此类推。看下面这张示意图用文本画一下原矩阵 1 2 3 4 5 6 转置后 1 4 2 5 3 6规律就是result[j][i] original[i][j]。如果你没绕明白写代码很容易把行列下标搞反。3.2 二维数组的输入与存储我要在这里重点讲一个编程细节“二维数组”在内存中虽然是连续存储的但逻辑上你可以把它想象成一个表格。声明二维数组的方式是int matrix[10][10];前面是行数后面是列数。当时我自己最喜欢犯的一个低级错误就是循环嵌套时老把i和j搞混。后来我养成一个笨办法统一约定第一个下标是“行”的方向也就是从上到下第二个下标是“列”的方向也就是从左到右。只要这个约定不破循环嵌套再深也不会乱。下面的代码是矩阵转置的一个最简实现#include stdio.h int main() { int matrix[100][100]; int transposed[100][100]; int row, col; scanf(%d %d, row, col); for (int i 0; i row; i) { for (int j 0; j col; j) { scanf(%d, matrix[i][j]); } } for (int i 0; i row; i) { for (int j 0; j col; j) { transposed[j][i] matrix[i][j]; } } for (int i 0; i col; i) { for (int j 0; j row; j) { printf(%d , transposed[i][j]); if (j row - 1) printf(\n); } } return 0; }3.3 边界值分析空矩阵和单元素矩阵这里有个很有意思的边界情况。如果输入是一个1行1列的矩阵转置前后一模一样代码能正常跑吗答案是能但前提是你的循环条件里没有“减一”之类的操作。很多同学在输出时用“如果遇到最后一个元素就不输出空格”的办法单元素情况就容易出问题。我当时遇到的另一个很微妙的坑是判断换行。上面代码中我用if (j row - 1) printf(\n)来控制换行。实际上这样写比“每次输出空格然后最后删掉末尾多余空格”要容易很多。不过也要注意printf(%d , transposed[i][j]);会在每个数字后面带一个空格这个空格是否会被判题系统接受一般而言OJ对行末空格是宽容的但如果你不确定尽量用不输出末尾空格的写法。3.4 我建议的分层调试方法二维数组一旦出错人眼很难在密密麻麻的输出中定位问题。我的做法是先打印“原矩阵”确认输入没有问题再打印“结果矩阵”检查转置逻辑是否正确。很多同学直接一步到位写完整程序错了也不知道哪里错。把大问题拆成小问题逐个验证是以后做任何复杂项目的必备思维。4. 第18题字符处理与ASCII码最容易忽视的空字符陷阱第18题的内容往往开始涉及字符char和字符串的处理了。这会开启一个新的世界因为字符在C语言里本质上是一个整数字符对应的ASCII码值所以很多操作很灵活但同时也带来了不少麻烦。4.1 字符数组还是字符串函数C语言里的“字符串”其实就是一个以空字符\0结尾的字符数组。很多人初学的时候并没有真正理解这一点在操作字符数组时没有留出\0的位置导致输出时出现乱码或者烫烫烫。第18题比较典型的出题方向有统计各类字符个数、大小写转换、回文判断等。我这里拿“统计数字字符和字母字符个数”来举例因为它很基础又能很好体现字符判断的思路。比如题目给你一行字符串里面掺杂着字母、数字、空格它可能要求你输出数字字符有多少个字母字符有多少个。这题的核心逻辑非常固定读取字符串注意不是逐个读字符遍历每一个位置用条件判断它属于哪类字符。代码长这样#include stdio.h #include string.h int main() { char str[1000]; // 考虑到字符串中可能包含空格用gets或fgets读取 // 注意online judge常用gets但gets不安全更推荐fgets while (fgets(str, 1000, stdin) ! NULL) { int letters 0, digits 0; int len strlen(str); // 去掉末尾的换行符 if (str[len - 1] \n) { str[len - 1] \0; len--; } for (int i 0; i len; i) { if ((str[i] a str[i] z) || (str[i] A str[i] Z)) { letters; } else if (str[i] 0 str[i] 9) { digits; } } printf(letters%d, digits%d\n, letters, digits); } return 0; }4.2 读取字符串gets、scanf、fgets到底选哪个这里一定要展开说一下。很多OJ题目尤其是基础题当年给的标准解法是gets()函数。但gets()不安全因为它不检查缓冲区大小可能会造成越界访问。现代编译环境已经开始不推荐甚至有的环境直接移除了这个函数。所以如果你在刷题时遇到“读取一行字符串且包含空格”的需求我建议你用fgets()。它有三个参数目标字符数组、最大读取长度、读取的文件指针标准输入就是stdin。要特别注意的是fgets()会把换行符\n也读进来所以上面代码里有一个“去掉末尾换行符”的步骤这一步漏了字符计数就会出现“多1”的诡异错误。4.3 用字符本身做比较而不是死记ASCII码有些同学学到现在还不理解为什么可以写str[i] a这里其实就是拿字符的ASCII码值做比较。字符常量在内存里就是整数比如字符a的ASCII码是97z是122A是65Z是900是489是57。我见过有人写str[i] 97 str[i] 122这也能跑通但可读性极差。与其记数字不如直接用字符常量去比较别人看代码一目了然也不容易因记错数值而出bug。4.4 字符大小写转换的一个小坑顺带提一个很容易在类似题目中考到的小点大小写转换。如果你想将大写字母变成小写正确姿势是利用ASCII码的差值ch ch 32。因为A 65a 97刚好相差32。但要注意修改前必须先判断它确实是大写字母否则乱加32会把数字或符号变成奇怪的东西。这个“判断后再转换”的习惯和第16题里的“遍历-判断-操作”是一脉相承的。第18题的设计初衷就是让你在字符层面也建立起类似的流程化编程思维。5. 刷这三道题时我真实踩过的那些坑讲完了每道题的技术细节我集中分享几个我在东华OJ刷基础16-18题时真实踩过的坑和排查思路。这些在教科书和题解里很少被提到但非常影响实际做题体验。5.1 运行时错误Runtime Error的排查链路有一段时间我提交第17题矩阵题时每次都是Runtime Error而本地运行完全正常。后来反复排查才发现问题出在数组大小上。我开的是int matrix[10][10]但题目的矩阵大小实际可以达到50甚至100。一旦输入数据超过了声明的大小程序就会访问到数组以外的内存区域这就是典型的越界。所以我现在有一个习惯看题目的数据范围然后数组开得比最大范围大一点点。题目说n不超过1000我就开1005或1010给边界留一点余量这在所有的OJ题里都是安全策略。5.2 格式错Presentation Error比答案错更折磨人有时候你算出来的结果完全正确但提交后反馈是Presentation Error通常缩写PE。这个状态的含义是你的输出和标准答案只在空白字符空格、换行上不一致。东华OJ对格式有严格要求所以这时候一定要翻出题目描述逐字逐句看它的示例输出。我记得有一次题目要求两个数字之间用逗号分隔我没注意到用了空格于是反复PE。当时心情很崩溃但后来学到一招当你看到一个输出示例时把它复制到记事本里打开显示所有字符就能看清楚哪里是空格哪里是回车。这个方法直到现在我做题目时都还在用。5.3 多组输入情况下“上一组的残留”问题处理第16题和第18题时“多组输入”是个隐形杀手。最典型的表现是第一组数据结果正确从第二组开始结果就变得莫名其妙。十有八九是你某个统计变量没有在循环体内重新归零。比如你统计字符个数如果letters和digits定义在while循环外面又不在每组开始时重置为0那么第二组统计时就会把第一组的数据加上去。我当时为了这个问题抓狂了很久后来总结的规则是如果某组数据需要独立的统计结果相关变量尽量定义在循环体内部这样每次循环开始都会自动重新初始化完全不用手动清零极大降低出错概率。6. 关于基础题的一些心态建设与进阶指引刷题刷到后来我越发觉得OJ基础题最大的价值不是让你学会某个特定的语法而是磨炼一种“冷静拆解问题”的态度。6.1 不要因为AC了就草草了事我见过很多人代码通过之后就立刻翻下一篇题解或者开始下一题。但我个人非常不建议这么做。一道题通过只代表你的代码在特定测试数据下是对的不代表你的设计思路是优雅的。我建议每道题AC之后至少再问自己三个问题如果数据量扩大100倍我这个算法还能通过吗如果输入极端的边界情况比如n1、数组全相等我的代码会出错吗我的代码是否让别人能轻松看懂这三个问题也是基础题连接进阶题目的桥梁。6.2 基础题和真正竞赛题的区别与延续第十六题到第十八题在东华OJ里只是基础篇里的几小步但它们涉及的三类核心能力在后面的算法学习中会不断出现数组后续升级为更复杂的数据结构、矩阵坐标变化后续升级为图论搜索中的方向数组、字符处理后续升级为字符串匹配算法。每一次在基础题里养成的习惯都是在给以后铺路。举个例子我在第17题里养成的“严格区分行列下标”的习惯后来转换到迷宫搜路径的上下左右移动时一点都没有陌生感因为核心都是坐标变化的对应关系。6.3 独立完成比AC本身更重要这里我想特地说一句代码通过不是最终目标独立写出来才是最终目标。很多同学做OJ练习的时候先看题解再敲代码甚至直接复制然后看到Accepted就很有满足感。这种事情我没少干过但效果真的非常差。学了半个月之后回头一看写过的题全忘光了。正确的做法是拿到题目哪怕想半小时没有头绪也要强迫自己先写一个“不完整版本”。这个版本跑不通也没关系关键是你在思考你的脑子在建立神经连接。等到实在卡住再看题解你才会知道自己是卡在哪一步。这在以后无论做项目、调试线上Bug都是最重要的能力。7. 最后再分享一点调试的心得除了上面那些我还想分享一个提高刷题效率的调试习惯写代码时先不要追求完美先用“硬编码”验证思路。比如我在写第17题转置时会先用一个3行2列的自己造的简单数据在代码里赋值确认核心逻辑没有错误后再改成从标准输入读取。这能帮你省掉大量和数据格式纠缠的时间把注意力集中在算法本身。另外提交前用几个自己构造的边界测试数据去跑一下代码是个好习惯。比如如果题目要求n组数据测一下n1的情况如果输入字符串测一下全是空格的情况如果涉及矩阵测一下行和列相等的情况以及行和列不等的情况。这些边界数据往往比随机数据更能暴露问题。遇到Runtime Error先看数组越没越界遇到Wrong Answer先检查输出格式、变量初始化、逻辑条件边界这三类最常出问题的地方。我始终觉得刷OJ题和写业务代码有一个共同点你的每一次通过都不是运气而是你控制变量的能力在提升。这三道基础题是很多人和这种能力第一次正面相遇的地方。希望这篇东西能帮你在东华OJ的基础关里少走一点弯路踏踏实实地把编程的底子打好。