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,你的程序必须自行处理多组数据直到读到EOF(End 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数组如果没在末尾手动加'\0',printf("%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; }答案是多少?是9(1+3+5)。但考试时很多人心算到一半就开始飘了,把continue和break弄混,或者把循环边界数错,结果写出个8。这种题不考算法,考的就是你对控制流极度精确的把握,没有任何狡辩空间。
4. 打开"2.23"这道题:一道典型的复合应用题拆解
好,到了最关键的环节。标题里提到的"2.23",我倾向于理解它是这场2月23日考试里比较有代表性的一道题——这种编号命名方式在吉大OJ历年题单里很常见,用日期做标识,方便同学考后复盘时引用。我不能百分百确认它最后考的原题长什么样,但根据"程序设计基础"的考纲和出题风格,我可以很负责任地告诉你,这类题目的套路是高度可预测的——它十有八九是一个"模拟 + 数学 + 字符串/数组处理"的复合题目。
我带你把一道典型的这类题完整走一遍。以下题目是我基于常考模型重构的参考样题,但解剖方法可以平移到考场上任何一道类似的题上。
4.1 先看一道"长相"类似的典型题
题目描述:小明最近在研究一个数学游戏。给定一个正整数 n,把 1 到 n 之间的所有整数按照从小到大的顺序连成一串,得到一个长字符串。例如 n=11 时,连成的字符串是 "1234567891011"。现在输入一个数字 k(1 ≤ 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循环在n=0时直接跳过,统计结果永远是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个单位的空间,能在很多边界情况下救你一命,比如某些循环里用了i+1作为下标访问。
第二个问题:输入输出和题目要求的格式一致吗?很多题目要求输出"Case 1: xxx"这种带编号的格式,我也经常写完主体逻辑就忘了这茬。所以每道题写完后我会专门检查一遍printf语句,确认中间的空格、冒号、换行一个不差。
第三个问题:有没有多余的调试输出?这是最冤的丢分方式。本地调试时我总会加一堆printf("debug: %d\n", i);,提交前如果忘了删,OJ会把调试信息一并当作答案输出,直接判错。所以我有一个肌肉记忆:提交之前先按Ctrl+F搜索"debug"和"printf",确保没有残留。
5.4 第四步:测试数据别懒,分成四类来测
很多同学写完代码,拿题目给的样例输入一跑,发现一样,马上就交。这是大忌。题目样例只能说明你的代码能跑通"最温柔"的数据,真正的考验在于那些它没给你的数据。
我每次提交前,至少跑四类测试数据:
- 样例数据:这是基准,如果连这个都过不了,说明基本逻辑有问题;
- 最小数据:比如n=1、k=1,或者输入为0的特殊情况,检验边界处理;
- 最大数据:比如n=1000000、k=9,看看程序会不会超时、数组会不会溢出;
- 随机数据:写一个小脚本生成一堆随机输入,再写一个暴力解法程序,两者输出对比,完全一致才敢提交。
你可能会觉得第四类太麻烦,期末考场上哪有时间搞暴力对拍?确实,如果时间紧张,可以退一步,至少构造几组自己手算过答案的数据,把输出和手算结果比对一下。这一步花不了两分钟,但能帮你躲过至少一半的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。这些东西,都是那场考试真正教会我的。