1. 一次期末考试的神转折2022级《程序设计基础》到底考了什么先把话说在前面如果你正在备战吉大《程序设计基础》的OJ期末考试或者你单纯想看看大学低年级的程序设计课能考到什么程度这篇东西应该对你有用。我写这篇文章不是要给你押题而是把2022级这次期末考试的考察逻辑、题型套路、以及我认为最有代表性的那道题——标题里的2.23——彻底拆开讲清楚。先说结论这次考试整体难度不算变态但很磨人。所谓磨人就是它不考偏题怪题而是把教材上你自以为懂、但代码一写就漏洞百出的知识点换着花样往OJ上扔。2022级这场考试放在2月23号本身也有点特殊——它是2022年秋季学期受整体环境影响、顺延到春季开学初考的。这意味着什么意味着你比正常情况多了一个假期的时间准备也意味着老师有更充裕的时间把题目打磨得更有区分度。从结果来看确实打磨得挺细。整张卷子给我的感觉可以用三句话概括第一基础语法占比高但绝不白给。别以为考switch、考for循环就是送分题它会在循环边界、数据类型溢出、输入输出格式这些地方做文章一不留神就是一次Wrong Answer。第二算法思维集中在模拟 简单数学 字符串处理三块。没有动态规划没有图论甚至没怎么考递归的复杂分支但每道题都需要你踏踏实实地把过程一步步模拟清楚。第三OJ机制本身就是一道隐形大题。很多人不是不会写代码而是不会和OJ打交道——多读了一个换行、数组开小了一格、循环里多了一次无意义的比较都可能让你在罚时里淹死。这篇文章我就从这三点出发带你把2022级这场考试从里到外过一遍。尤其是那道和2.23相关的题目我会完整走一遍从读题到AC的思考链路把我当时踩的坑、绕的弯也一并交代了。如果你是低年级学弟学妹这篇文章可以当备考地图用如果你已经工作多年、纯粹想回顾一下大学编程课的味道那咱们就当聊天边看边回忆当年被OJ支配的日子。2. 为什么考试形式是OJ这件事本身就是最大的考点很多人第一次听到期末考试用OJ时的反应是不就是把纸质卷子搬到电脑上吗考试范围一样题型一样换个载体而已。真不是这样。OJ考试和纸质考试的底层逻辑完全不同理解这一点你才算拿到了这场考试的入场券。2.1 OJ考试的评分机制它对正确的定义比你想的更苛刻纸质考试里你写一段代码只要思路对、逻辑通哪怕少写一个分号、变量名拼错了老师都会酌情给分。但OJ不是老师它是一个没有感情的裁判机器。它的评分逻辑只有一条你的程序能不能在规定的时间和内存内对评测数据给出完全正确的输出。中间有任何偏差——多一个空格、少一个换行、大小写不一致、浮点数精度差了一点点——统统判错。所以OJ考试本质上是在考工程化的严谨性。它逼着你养成的第一个习惯就是别觉得自己看懂了题就能拿分你的代码必须精确到每一个字节。拿字符串输出来说题目要求输出Case 1: 5和Case 2: 10你输出成Case 1:5冒号后面少个空格本地跑起来毫无问题一提交就是Presentation Error。这个错误在OJ里算是温柔的提醒说明你的答案已经非常接近了只差格式。但在考试场景下它依然会消耗你的时间积少成多非常影响心态。2.2 2022级这次考试的运行环境和提交规则我印象里吉大OJ考试用的是标准GCC/G编译环境支持C和C两套语言绝大部分同学选的是纯C。考场有统一的提交接口代码写完后通过网页或IDE插件提交系统实时返回评测结果。这里有一个很多人忽略的细节OJ评测时的输入不是你在键盘上一个一个敲进去的而是评测系统把预置好的测试数据直接喂给你程序的stdin你的程序必须自行处理多组数据直到读到EOFEnd of File文件结束符为止。于是循环读入、读到文件尾停止就成了考试的第一道坎。很多第一次接触OJ的同学平时练习用的是Dev-C习惯性在程序里写一句scanf(%d, n);然后开始处理完全不考虑如果输入有多组数据怎么办。考试题里凡是没说明只有一组数据的基本都是多组输入你得会这样写int n; while (scanf(%d, n) ! EOF) { // 处理每组数据 }还有输入一串字符的情况注意gets函数在新版C标准里已经被移除了老老实实用fgets或者scanf配合正则读法。这些细节单独拿出来都不是事儿但合在一起就是试卷上那张看不见的基础分。2.3 罚时机制为什么先做简单题是考试策略里的重中之重吉大OJ考试用的是ACM赛制的话会带罚时统计——也就是说你每提交一次错误的代码除了本题不得分还会在最终排名上加一笔时间惩罚。当然如果只是期末考、最终按通过题数算分的话罚时可能不影响卷面分。但如果是按排名给平时分或附加分罚时就很要命了。我的建议是无论有没有罚时都按有罚时的方式来应对先扫一遍全部题目挑最简单的做把保底分拿住再回头啃硬骨头。实在做不出来的题宁可不提交也别瞎交上去骗评测——因为它除了给你一个红色的Wrong Answer什么帮助都没有。正确的调试方式应该是自己构造边界数据在本地把bug揪出来而不是拿OJ当调试器用。3. 题型全景从选择结构到数组字符串难度是怎么分层设计的这一节我尽量还原2022级这次考试的题型分配。虽然我不能把每道题的原题一字不差地默写出来也没必要毕竟考试年年变但根据考过的同学反馈和这套课程一贯的出题风格题型框架是相当清晰的。3.1 第一梯队选择、循环、函数题——送分但不完全送这类题大约占3到4道每题考察一个基础点。比如给你一个年份判断是不是闰年或者模拟一个简单的累加器。看起来人畜无害但老师会在微妙的地方加一点作料。以闰年题为例最经典的坑是这样的年份能被4整除但不能被100整除是闰年年份能被400整除也是闰年其他情况不是闰年。很多人记成四年一闰百年不闰就完了结果1900年这种整百但不能被400整除的年份就出错了。这种题在纸上写着你可能不会错但在考场的高压和限时下人很容易不假思索地写year % 4 0就收工。再比如函数题一般考的是编写一个函数在main里调用。考点是参数传递和返回值。C语言的函数参数默认是值传递你传一个整数进函数函数内部怎么改都不影响外面的变量。但如果你传的是数组名那本质上传的是指针函数内部是可以改到原数组的——这个区别很多人当时根本没想明白导致写出函数运行完主调函数里的数组没变这种诡异bug。void swap(int a, int b) { // 错误写法值传递换了个寂寞 int tmp a; a b; b tmp; }正确的交换应该传指针或者用返回值。这类题我不展开太多因为真正拉开差距的不在这里。3.2 第二梯队数组操作和字符串处理——真正的分水岭这个梯队大概有3道题左右是整张卷子区分度最高的地方。它不像第一梯队那样一眼看穿需要你结合循环、条件判断和数组/字符串的底层存储逻辑来综合解决。典型考法包括数组去重给一个数组输出去重后的结果要求保持原顺序矩阵操作矩阵转置、主对角线元素求和、周边元素求和字符串统计统计一串字符里各类字符字母、数字、空格、其他的出现次数或者统计单词个数字符串翻转不借助额外数组原地翻转一个字符串。这些题难吗单独拿出来任何一个写过两位数题量的同学都能做出来。但在考试环境下它的难点在于你的脑子要同时处理算法逻辑和C语言语法细节两件事任何一个分心写出来的代码就有隐蔽bug。举一个最常见的例子——字符串翻转。最容易想到的思路是用一个新数组从原字符串末尾往前读逐个放进新数组里。这个思路没问题但实现的时候很多人栽在字符串结尾的\0上。#include stdio.h #include string.h int main() { char s[100], rev[100]; gets(s); // 不推荐仅为演示 int len strlen(s); for (int i 0; i len; i) { rev[i] s[len - 1 - i]; } rev[len] \0; // 这句忘写了输出就是一串乱码 printf(%s\n, rev); return 0; }rev数组如果没在末尾手动加\0printf(%s)会因为找不到字符串结束符而一直往后读内存直到碰到一个随机的0为止。这种错误在本地跑一次就能发现但如果考试时你图快没仔细测就直接交了那就是一次无谓的罚时。3.3 第三梯队程序填空和结果填空题——看起来简单实际最阴吉大OJ考试里还有一个特有题型就是程序填空和读程序写结果。它给出一段不完整的代码或完整代码让你补充几个空或者直接写出运行结果。这类题看着不用写代码好像轻松一点实际上它比纯编程题更容易暴露理解不深的问题。举个例子一道经典的程序填空#include stdio.h void fun(int a[], int n) { for (int i 0; i n - 1; i) { for (int j i 1; j n; j) { if (a[i] a[j]) { // 填空交换a[i]和a[j] } } } }很多同学在上面填int tmp a[i]; a[i] a[j]; a[j] tmp;这本身没有错但不全对——因为C语言里如果在for循环体内声明变量其作用域只限于这个循环体内部上面这个写法是合法的。可如果题目把交换写成一个独立的sawp函数调用函数参数又没传对那就出问题了。程序填空题的阴险之处就在这里它给你一个看似正确的语境让你在代码的犄角旮旯里找出真正需要补全的那个点同时提防着你顺手把不该动的部分也改了。读程序写结果就更不用说了它考的完全是人肉执行C语言的能力。#include stdio.h int main() { int i, s 0; for (i 1; i 5; i) { if (i % 2 0) continue; s i; } printf(%d\n, s); return 0; }答案是多少是9135。但考试时很多人心算到一半就开始飘了把continue和break弄混或者把循环边界数错结果写出个8。这种题不考算法考的就是你对控制流极度精确的把握没有任何狡辩空间。4. 打开2.23这道题一道典型的复合应用题拆解好到了最关键的环节。标题里提到的2.23我倾向于理解它是这场2月23日考试里比较有代表性的一道题——这种编号命名方式在吉大OJ历年题单里很常见用日期做标识方便同学考后复盘时引用。我不能百分百确认它最后考的原题长什么样但根据程序设计基础的考纲和出题风格我可以很负责任地告诉你这类题目的套路是高度可预测的——它十有八九是一个模拟 数学 字符串/数组处理的复合题目。我带你把一道典型的这类题完整走一遍。以下题目是我基于常考模型重构的参考样题但解剖方法可以平移到考场上任何一道类似的题上。4.1 先看一道长相类似的典型题题目描述小明最近在研究一个数学游戏。给定一个正整数 n把 1 到 n 之间的所有整数按照从小到大的顺序连成一串得到一个长字符串。例如 n11 时连成的字符串是 1234567891011。现在输入一个数字 k1 ≤ k ≤ 9请统计这串长字符串中数字 k 一共出现了多少次。输入包含多组测试数据每组占一行包含两个整数 n 和 k以 EOF 结束。输出每组数据中数字 k 出现的次数。考察点非常清晰你怎么处理把整数拼接成字符串这一步。这是典型的字符串与数组结合题在考试里比单纯给你一个字符串让你数频率要难一个档次因为中间的转化步骤需要你自己想。4.2 错误率最高的解法别急着用 itoa很多同学的第一反应是先建一个大数组把每个整数转成字符串拼进去然后遍历统计。思路没错但itoa不是标准C函数在部分OJ环境下根本编译不过。就算编译过了你还要自己管理缓冲区大小整数转字符串的边界情况也容易处理漏。与其用工具函数不如换个思维角度统计数字出现次数并不需要真正把字符串拼出来。我们可以逐个整数处理把每个整数的每一位拆出来统计。#include stdio.h int main() { int n, k; while (scanf(%d %d, n, k) ! EOF) { int count 0; for (int i 1; i n; i) { int x i; while (x 0) { int digit x % 10; if (digit k) { count; } x / 10; } } printf(%d\n, count); } return 0; }这段代码的逻辑非常直白对每个数i不断除10取余拆出每一位数字和k比对。它的时间复杂度是O(n × log₁₀n)当n不大时完全够用。但这里有一个隐藏的大坑当i本身就是0的时候while循环一次都不会进入。样例里如果n从0开始计数你需要把数字0的那一次也统计进去。2022级这道题的表述里n是从1开始的还是从0开始的我没法替你确认你自己上考场时一定要把题干的区间定义看仔细。4.3 进阶如果题目范围变大你还得会优化这类模拟题还有一种升级版——n的范围特别大比如n可以到10⁹甚至更大。这时候逐个数枚举一定会超时你必须有数学思维统计数字在某一位上出现的次数是可以用组合数学直接算出来的。这个方法是经典的按位计数法。以统计1到n中数字k在十位上出现的次数为例思路是把n拆成高位、当前位、低位三部分。对于十位上的每个数字循环它对答案的贡献由以下规则确定如果当前位大于k那么这一位上数字k出现(高位 1) × 10^低位位数次如果当前位等于k那么这一位上数字k出现高位 × 10^低位位数 (低位 1)次如果当前位小于k那么这一位上数字k出现高位 × 10^低位位数次。我当时复习到这里的时候第一次看这个公式完全懵了后来自己手动算了一遍才想明白。举个例子n 213统计十位上数字1的出现次数。十位当前位是1高位是2低位是3套用第二条规则次数 2 × 10 3 1 24。这24次对应的是十位从0到2变化时每种情况下个位跑满0到9其中10到19这一整段贡献了完整的10次而210到213这段贡献了4次210、211、212、213。如果当前位是0比如n 203十位上的1出现次数就是高位2× 10 20次对应10到19、110到119这两段。这种数位计数的方法对第一次见的同学来说需要一点消化时间但它正是期末考里拉开差距那道题的真面目。如果你只掌握了暴力枚举那恭喜你你能做对70%的测试点拿一半分但想拿满分你得学会这种数学优化。我个人建议各位至少把暴力法写到滚瓜烂熟——考试时先把暴力交上去保底有时间再尝试优化版本这才是最稳的策略。4.4 边界条件这类题真正的灵魂裁判构造测试数据时最爱干的事情就是穿插边界样例。对于统计数字出现次数这类题典型边界包括n 0或n 1时程序行为是否正常。很多人的while循环在n0时直接跳过统计结果永远是0可实际上数字0可能是需要计数的。k 0时是否被单独处理。统计0的出现次数比统计1到9要复杂得多因为数字本身如果以0开头比如把5写成05是不算出现了一个0的。如果你的代码逻辑是从低位一直除到0为止那它天然避开了这个问题但如果你用了补前导零的字符串拼接法就会把本来不存在的0也算进去。大数溢出。如果n很大count变量用的是int统计结果可能超过21亿直接溢出变负数答案错误。正确做法是开long long尤其在优化版本里中间乘法结果更是容易爆。long long count 0; // 别用int除非你确定数据范围足够小我把这道题完整拆开来说是想传递一个观念OJ考试里的所谓难题其实就是多个基础坑的叠加。你提前把这些坑都填平了到了考场上就不会慌。5. 考场实战我当时是怎么一步步从读题到AC的前面题型和例题都说得差不多了这一节我想换一种叙述方式带你们沉浸式走一遍考场的解题流程。都说授人以鱼不如授人以渔我现在把渔的过程完整演示一遍。5.1 第一步把题目翻译成人话拿到任何一道题第一件事不是打开编译器而是拿出一张草稿纸把题目用自己的话重新描述一遍。比如刚才那道统计数字出现次数的题我会在草稿纸上写输入两个数n和k把1到n每个整数拆成单个数字统计其中等于k的个数。多组输入读到EOF结束。这一步看起来傻但特别有用。它强迫你抓住题目的最核心需求避免被题目长长的背景故事带跑偏。很多同学考试挂分不是不会做而是被题目里小明小红的故事绕晕了漏掉了某个条件。5.2 第二步根据数据范围选算法草稿纸上写完人话版题目之后紧接着标出数据范围。数据范围是选算法的唯一依据n ≤ 10⁶暴力枚举每个数除10拆位稳过n ≤ 10⁹必须用数位计数否则超时。说实话期末考试里给到10⁹级别的概率不是很高因为这门课的重点不在算法竞赛而在程序设计基础。但万一碰到了你得有个心理准备。我会建议你把暴力的框架先搭好把能拿的分拿到然后再考虑优化。5.3 第三步写代码时一边写一边灵魂拷问我写OJ代码的速度不慢但我有一个习惯每写五行就会停下来问自己三个问题。第一个问题数组越界了吗开数组时我习惯在题目要求的基础上多加5到10个单位的余量。比如题目说n ≤ 100我就开int a[110]。多出来的这10个单位的空间能在很多边界情况下救你一命比如某些循环里用了i1作为下标访问。第二个问题输入输出和题目要求的格式一致吗很多题目要求输出Case 1: xxx这种带编号的格式我也经常写完主体逻辑就忘了这茬。所以每道题写完后我会专门检查一遍printf语句确认中间的空格、冒号、换行一个不差。第三个问题有没有多余的调试输出这是最冤的丢分方式。本地调试时我总会加一堆printf(debug: %d\n, i);提交前如果忘了删OJ会把调试信息一并当作答案输出直接判错。所以我有一个肌肉记忆提交之前先按CtrlF搜索debug和printf确保没有残留。5.4 第四步测试数据别懒分成四类来测很多同学写完代码拿题目给的样例输入一跑发现一样马上就交。这是大忌。题目样例只能说明你的代码能跑通最温柔的数据真正的考验在于那些它没给你的数据。我每次提交前至少跑四类测试数据样例数据这是基准如果连这个都过不了说明基本逻辑有问题最小数据比如n1、k1或者输入为0的特殊情况检验边界处理最大数据比如n1000000、k9看看程序会不会超时、数组会不会溢出随机数据写一个小脚本生成一堆随机输入再写一个暴力解法程序两者输出对比完全一致才敢提交。你可能会觉得第四类太麻烦期末考场上哪有时间搞暴力对拍确实如果时间紧张可以退一步至少构造几组自己手算过答案的数据把输出和手算结果比对一下。这一步花不了两分钟但能帮你躲过至少一半的Wrong Answer。5.5 第五步提交后别干等接下来做下一题这是一个考场心态问题。提交之后OJ返回答案需要几秒到几十秒不等很多人就盯着屏幕干等Submission状态一出来发现错了心情立刻变糟然后死磕同一题死磕到考试结束。正确做法是提交完立刻把这道题抛到脑后去做下一题。等一段时间后集中回来查状态如果有错再根据错误类型编译错误、答案错误、超时、运行错误针对性地调试。这样你的时间利用率是最高的也不会因为一道题卡住而崩掉心态。6. 除了考试本身这场2.23给你留下了什么题目做完了分数拿到了这场考试就算翻篇了。但我个人觉得复盘比考试本身更有价值因为程序设计基础这门课的终极目标不是期末考高分而是让你建立一种与计算机对话的思维方式。这场2月23日里的考试至少用它的题目分布给所有2022级同学上了一场关于严谨性的课。6.1 从考试反推学习路径哪些能力是真正被看重的你回头看这套题闰年判断考的是条件逻辑的完备性循环累加考的是控制流理解字符串翻转考的是对内存模型的把握统计数字出现次数考的是问题拆解和边界思维。这些东西哪一样都不是靠考前突击背代码能解决的它要求你在平时的上机练习里就养成好习惯。我当年学这门课的时候老师说过一句话让我记到现在不要做搜索引擎的搬运工要做问题的拆解者。很多同学一遇到不会的题就上网搜代码抄一遍AC了就觉得自己会了下次换个马甲出现照样不会。正确做法是拿到题先自己思考半小时实在做不出来再搜别人的代码然后逐行理解它为什么这么写最后关掉参考代码自己重新从零实现一遍。这个过程很痛苦但效果奇佳。6.2 关于有效题量刷100道水题不如精做30道提到上机练习就绕不开题量焦虑。有人一天刷十道题有人一周做三道题你觉得谁期末考得好答案不一定。因为刷题的质量远比数量重要。我这里给一个可操作的精做流程第一步独立写出AC代码。不管耗了多久一定要靠自己在OJ上拿到Accepted这一步能保证你是真的理解了题目。第二步看别人的优秀代码。去OJ的讨论区或者网上的题解找一个和你思路不同、但代码更简洁或更容易理解的版本逐行对比找出差距。第三步一题多解。同一个问题尝试用不同的算法或数据结构重新实现一遍。比如统计数字出现次数暴力法会了再学数位计数法递归版斐波那契会了再写迭代版。这样你练的不是某一道题而是某一种思考维度。第四步写解题笔记。这步很多人嫌麻烦但强烈建议做。不需要长篇大论记下题目类型、核心思路、踩过的坑就够了。期末复习时翻一遍比重新刷十道题都有用。6.3 给未来考生的几句实在话说句掏心窝的话如果你现在看到这篇文章是准备未来参加这门课的期末考试那么我建议你注意以下三件事。第一件别把OJ考试当成上机编程它就是一场带编译器的笔试。你的代码要过的是机器那一关机器不会通融所以代码之外的格式、边界、性能一样都不能少。第二件提前熟悉考场OJ的提交界面和反馈信息。每次提交后你会看到Accepted、Wrong Answer、Time Limit Exceeded、Memory Limit Exceeded、Runtime Error这些英文单词。别等到考场上才第一次见它们平时做题时就把每个反馈对应的原因摸清楚这样考场上一眼就能定位问题方向。第三件也是最重要的身体和心态是最大的变量。考场上代码写不出来可以一步一步调试但如果你前一天熬夜考试时脑子一团浆糊那真的有再多技巧也使不出来。程序设计期末考试拼的不只是智力还有你在长时间高压状态下的稳定性。6.4 一个值得长期保留的调试习惯最后我想分享一个我从这场考试之后一直保留至今的调试习惯写程序的时候把防御性思维刻进潜意识。什么叫防御性思维就是每当你写完一个循环、一个数组访问、一次指针操作都下意识地问一句这里会不会越界这里会不会死循环这里如果输入是空会怎样这种思维不是天生就有的必须经过大量刻意练习才能内化。但一旦形成了你写出来的代码bug率会直线下降不只在OJ考试里受益将来做项目、写工程代码同样受益无穷。我从那场2.23之后再没犯过数组开小导致Runtime Error的低级错误也再没因为忘删调试输出而白送一次Wrong Answer。这些东西都是那场考试真正教会我的。