尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

东华OJ刷题46-50:C语言基础坑点与AC代码复盘

东华OJ刷题46-50:C语言基础坑点与AC代码复盘 东华OJ刷到46-50这一带的时候我突然意识到一个很现实的问题单纯会写代码还不够还得让代码学会跟OJ这个“铁面判官”打交道。前45题可能还在熟悉语法到这批题目它已经开始考验你输入输出处理、边界条件、甚至一点简单算法设计了。这篇文章是我自己刷完46-50之后整理的自用复盘里面有每道题我的理解、AC代码和踩过的坑也有我对东华OJ判定规则的观察。如果你正好刷到这一带或者刚接触在线评测系统想把基础打牢这份笔记应该能帮你少走几趟弯路。需要说明的是东华OJ的题号在不同时期可能略有调整我下面描述题面是按我的记忆写的具体以你在OJ上看到的题目为准。但解题思路和代码模板是通用的你完全可以把方法搬到其他类似的题上。1. 先给这五道题画个像46到50到底在考什么很多人一提到刷OJ开口就是“今天AC了几道”好像AC数量就是一切。但刷到46-50这批题我才慢慢体会到AC只是及格线真正的收获在于你知不知道那一次提交为什么错以及错了之后怎么定位。这五道题给我的整体感觉是难度比前面明显抬了一小档但还没有到需要复杂算法的程度。它更像是一组“基础技能体检”考察的无非是数字处理、判断逻辑、字符串、数组排序和递归递推。我把印象中的题目类型和核心考点整理成了下面这张表方便你对照题号按我记忆题目类型核心考点我提交了多少次才过46求各位数字之和循环取余、多组输入247判断素数并输出素数判定、迭代边界348统计字符串中的数字个数字符串读取、字符处理449排序后从大到小输出排序算法、C库函数250求斐波那契数列第n项递归/递推、溢出防范5我印象最深的是第50题表面上是斐波那契代码可能十行不到但我连续WA了三次。后来发现不是公式问题而是数据范围没考虑清楚int不够用必须换成long long输出格式也跟我想的不一样。这种“栽在细节上”的挫败感刷过OJ的人应该都懂。所以如果你想从这五道题里得到点什么我的建议是不要只盯着AC代码抄多花点时间复盘每一份错误提交背后的原因。这批题目最大的价值不是让你会写循环而是让你养成一种“代码还没提交就能猜到判题系统会怎么怼你”的敏感度。2. 逐一过题46到50的AC代码与解题逻辑这一部分我按题号顺序做复盘。每道题我会先复述一下我印象中的题面再讲解题思路最后给出我能一次通过的参考代码。代码我用的是C语言因为东华OJ最常见的就是C/C风格而且基础题用C写最能看清底层逻辑。2.1 第46题求一个整数的各位数字之和我印象中这道题的题面很简洁输入一个正整数N计算它的各位数字之和比如输入123输出6。稍微麻烦一点的是题目说了“多组测试数据处理到文件结束”意思是你要反复读入直到EOF。很多人第一版代码只处理了一组输入结果提交直接WA。核心思路就是一个while循环每次对10取余拿到当前最低位然后整除以10去掉最低位累加到一个变量里。C语言代码如下#include stdio.h int main() { int n; while (scanf(%d, n) ! EOF) { int sum 0; while (n 0) { sum n % 10; n / 10; } printf(%d\n, sum); } return 0; }这段代码有个细节如果输入本身就是0内层while一次都不会执行sum保持为0输出0这是对的。如果题目后来换了数据会出现负数那你可以考虑用abs()取绝对值或者先判断符号不然-123 % 10在C语言里结果是-3累加出来就会出错。虽然东华OJ这道题大概率只给正整数但养成处理负数的习惯没坏处。2.2 第47题输出n以内的所有素数印象中这题是输入一个正整数n要求从小到大输出2到n之间的所有素数每个素数占一行。素数这个概念不复杂但如果你老老实实对每个数都从2除到它自己那当n比较大的时候程序会卡到天荒地老。其实只要除到sqrt(x)就够了。我参考的判题逻辑一般是如果x小于2直接返回false从2到sqrt(x)逐一取模只要能整除就说明不是素数。代码可以这么写#include stdio.h #include math.h int is_prime(int x) { if (x 2) return 0; int limit (int)sqrt(x); for (int i 2; i limit; i) { if (x % i 0) { return 0; } } return 1; } int main() { int n; while (scanf(%d, n) ! EOF) { for (int i 2; i n; i) { if (is_prime(i)) { printf(%d\n, i); } } } return 0; }我记得我第一次提交这个题时忘了加#include math.h本地用VS编译完全没问题但OJ用的编译器对sqrt的隐式声明并不宽容直接CE了。后来养成了习惯用了哪个函数就把对应的头文件写全别指望编译器帮你补。如果你刷题时发现n的范围特别大比如大于10^6那这个逐个判定的写法可能就超时了。那时候你可以换成埃氏筛先把2到n的合数全部标记出来再输出没被标记的数。筛法的思路很简单从2开始把每个素数的倍数全部划掉剩下的就是素数。学习OJ的过程里能从一个简单题延伸到筛法已经算超额收获了。2.3 第48题统计字符串里的数字字符个数这题我印象中也比较典型输入一行字符串可能包含空格和标点让你输出里面数字字符的个数。比如输入“abc123d4”要输出3和4两个数字的总个数也就是4。这题对新手最不友好的地方不是统计本身而是怎么读入一行带空格的字符串。很多人上来用scanf(%s, s)结果只读到空格前的部分后面全被丢掉。很多基础题描述里不会特意提醒“字符串可能包含空格”但这恰恰是OJ最常见的隐藏条件。我当时的做法是用gets()虽然这个函数在现代C标准里有缓冲区溢出的风险但东华OJ老平台上很多基础题还是能用的。代码长这样#include stdio.h #include string.h int main() { char s[1005]; while (gets(s) ! NULL) { int cnt 0; for (int i 0; s[i] ! \0; i) { if (s[i] 0 s[i] 9) { cnt; } } printf(%d\n, cnt); } return 0; }如果你在OJ上测试时发现gets被禁用了那就用fgets(s, sizeof(s), stdin)但要记得它会把换行符也读进来统计数字时倒无所谓因为换行符不是数字不会被计数。还有一种可能题目要求输入多次直到读入某个结束标记比如读到“END”或者“0”那你就需要在循环里加个判断条件。读题时眼睛一定要放亮这个题的描述里通常会有“多次输入直到文件结束”或“直到输入停止”这样半句话。2.4 第49题把若干整数按从大到小排序这题给我的感觉像是“语言基础函数大礼包”。题面一般会给你一个整数n然后接下来一行有n个整数要求你把这n个整数按从大到小的顺序输出。排序本身并不难但如果你不懂C语言的qsort就得手写冒泡或选择排序。我当时先写了一版冒泡排序#include stdio.h int main() { int n; int a[1005]; while (scanf(%d, n) ! EOF) { for (int i 0; i n; i) { scanf(%d, a[i]); } 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; } } } for (int i 0; i n; i) { if (i ! 0) printf( ); printf(%d, a[i]); } printf(\n); } return 0; }冒泡排序的时间复杂度是O(n^2)当n只有一两千的时候完全没压力。但如果你看到题目里n范围能到10^5就别再冒泡了直接qsort它内部是快速排序平均复杂度是O(n log n)。qsort有一个坑就是比较函数怎么写int cmp(const void *a, const void *b) { return *(int *)b - *(int *)a; }从大到小就返回b减a从小到大就返回a减b。很多人第一次写会忘记先把void指针转成int指针结果编译一堆warning甚至直接报错。这题让我意识到除了会写算法还得会熟练使用C标准库不然遇到大数据量的时候只能干瞪眼。2.5 第50题求斐波那契数列第n项这题是我这一批里WA次数最多的一道所以展开说一下。题目本身很经典斐波那契数列的第1项和第2项都是1从第3项开始每一项等于前两项之和。输入一个n要求输出第n项。如果按最直的办法写递归long long fib(int n) { if (n 1 || n 2) return 1; return fib(n - 1) fib(n - 2); }代码确实漂亮但效率极其感人。n稍微大一点比如n50这个递归会重复计算无数次我本地跑都等了半天提交上去不是超时就是溢出。所以我后来改成递推#include stdio.h int main() { int n; while (scanf(%d, n) ! EOF) { if (n 1 || n 2) { printf(1\n); continue; } long long a 1, b 1; for (int i 3; i n; i) { long long c a b; a b; b c; } printf(%lld\n, b); } return 0; }这里最关键的是类型。斐波那契数列增长非常迅猛第46项已经超过21亿而int的最大值也就约21亿所以第46项就已经开始溢出了。第50项更是到了12586269025必须用64位的long long或者long long int。如果题目里的n还能更大比如n90那long long也扛不住了你就要考虑用高精度或者矩阵快速幂。这道题的教训让我认真记住了以后但凡看到序列、累加、幂运算这类题先估算数值范围再决定用什么类型。别等WA了才回头看是不是溢出这是在浪费时间。3. 为什么提交一直WA或PE东华OJ判定系统不会告诉你的细节刷到46-50这批题我最大的收获不是这几道题的解法而是搞清楚了OJ判题系统是怎么“想”的。你代码逻辑明明是对的但提交上去就是WA或者PE问题通常出在下面几个地方。3.1 多组输入的处理方式很多题目不会只让你处理一组数据而是用“多组测试数据处理到文件结束”或者“输入包含多行”来表述。如果你没有用while(scanf(...) ! EOF)包起来那OJ只会跑第一组测试数据后面的数据根本没执行到结果自然是错的。我自己的习惯是读完题目后先在格式部分找有没有“multiple test cases”或者“多组输入”的字样。只要出现这种描述就可以直接套这个框架while (scanf(%d, n) ! EOF) { // 你的逻辑 }如果题目给的结束条件是读到0或者读到某个字符串那就改成while (scanf(%d, n) 1 n ! 0) { // 你的逻辑 }这种输入框架几乎能应付东华OJ60%的基础题熟练掌握它比多背几道题答案都管用。3.2 输出格式的空格和换行OJ里有一种状态叫“Presentation Error”翻译过来是“格式错误”。我当时不太理解代码逻辑对输出内容也对怎么就PE了后来才明白OJ是把你的完整输出跟标准答案逐字对比的多一个空格、缺一个换行、最后多打印一个空行都会导致不通过。举个典型例子要求输出一行数字数字之间用空格隔开句末不能有多余空格。那就不能无脑在每个数字后面都加空格否则最后一个数字后面会多个空格。我一般会写成循环只有当前不是第一个元素时才打印空格for (int i 0; i n; i) { if (i ! 0) printf( ); printf(%d, a[i]); } printf(\n);还有一种情况是每个数字占一行这时候就简单了直接printf(%d\n, a[i])。我的经验是看题目描述里的输出示例示例里有空格就用空格有换行就用换行如果示例里最后一行后面没有多余空行那你的代码也不要输出空行。不要以为OJ看不出来它盯得比谁都仔细。3.3 数据范围和类型溢出基础题里最容易忽略的就是数据范围。我第一次刷50题斐波那契时就栽在这里。C语言的int大概只能存到21亿如果题目说n可以到50你就必须提前算一下结果会不会超。常用的参考值int上限约2.1×10^9long long上限约9.2×10^18double虽然能存更大数但会有精度问题OJ题里涉及整数输出时还是优先用64位整数。判断溢出有一个很土但有效的方法把代码里的关键变量换成long long再提交一次如果原本WA突然AC了那就是类型问题。我后来写题时只要看到、*、幂次、阶乘、斐波那契这类操作就会条件反射地使用更大范围的类型省掉很多来回提交的时间。3.4 我的调试三板斧当代码在本地跑样例没问题一提交就WA的时候我一般会做三件事第一本地构造几组极端数据比如最小的输入、最大的输入、有重复值的输入、空输入第二在关键位置加printf输出中间变量的值观察执行过程是不是跟预想的一致第三如果还是查不出来就停下来重新读一遍题很多WA都是因为没有看清输入范围或输出格式而不是算法错。我见过不少同学在一个题上反复提交十几次每次改几行再碰运气其实不如花5分钟静下心把代码一行一行口算执行一遍。OJ虽然不会告诉你错误原因但错误原因往往就藏在你不愿意看的细节里。4. 刷完这批题目后我沉淀下来的工具和方法46-50刷完除了AC数多了5个更大的收获是我形成了一套自己的“刷题交付标准”。下面这些是我现在写基础题时都会用到的模板和思考方式分享出来给你参考。4.1 一个干净的C语言提交模板我在东华OJ上提交时基本都从一个固定模板起步。它不花哨但能避免很多低级问题#include stdio.h #include string.h #include math.h #include stdlib.h int main() { int n; while (scanf(%d, n) ! EOF) { // 具体逻辑 } return 0; }这个模板有几个好处头文件尽量写全省得因为缺头文件CE主函数统一返回0多组输入用while包着。如果是字符串题我还会额外准备一个数组char s[1005]大小看题目范围定宁可开大一点也不要越界。你可能会说头文件写多了无所谓但有些OJ的编译器很严格缺头文件就真的不给过多写几行不会亏。4.2 时间复杂度的第一课46到50这批题里素数判断和排序已经涉及时间复杂度的概念了。我一开始也觉得基础题而已暴力不就完了后来看到题目的范围才意识到暴力不等于“无脑把所有情况都跑一遍”而是要在能过的前提下用最直接的方法。比如判断素数如果对2到n的每个数都从2除到n复杂度是O(n√n)当n10^6时已经有点危险了。但优化成2到√n后效率立刻翻了几十倍。再比如排序如果n只有100冒泡O(n^2)完全可以如果n是10^5就必须用qsort或自己写归并、快排。学会估算数据量级和算法复杂度是刷题从“碰运气”到“有把握”的分水岭。我常用的估算方法很简单1秒大概能跑10^8次简单运算。如果n是10^5n^2就是10^10妥妥超时如果是n log n大概10^6到10^7次能过。基础题不会卡得那么细但提前有这个意识后面刷到更难的题才不会慌。4.3 把一次AC变成长期收益很多人刷题只盯着“过没过”过了就丢错了就改改完也不复盘。我在46到50这批题上改变了自己的习惯每次AC之后我会把最终版代码保存下来文件名带题号和简单描述比如oj46_digit_sum.c然后在代码顶部写注释记录自己第一次提交为什么错。我保存下来的代码里注释一般长这样/* * 东华OJ 46 各位数字之和 * 第一次WA没有处理多组输入 * 第二次AC加上while(scanf ! EOF) */别小看这几行注释。两周后再翻回来你能一眼想起当时的坑而且这些坑大概率会在后面的题目里复现。比如“多组输入”这个坑我至少在后来的五六道题里又遇到过但因为我记了注释每次都能条件反射地写上while循环再也没栽过。还有一个习惯就是不定时翻看这些注释把相同类型的错误归类。刷到100题的时候你会发现常犯的其实就是那么几类输入格式看错、数组越界、溢出、输出多空格。这比背大量代码更有用。我个人的体会是OJ刷题到最后真正拉开差距的不是谁更聪明而是谁更细。46到50这五道题难度只能说中规中矩但它逼着我开始认真对待输入输出、数据范围和调试方法这些能力让我后面刷到数组、字符串、链表相关的题时轻松了很多。如果你也在东华OJ上走到了这个位置别急着追求AC数量先花点时间把这几道题里的“坑”填平把代码模板整理顺手后面会越刷越快。
返回列表