东华OJ做到第28~30题的朋友,基本都处在C语言函数和数组刚学完、开始碰综合题的阶段。这个位置的题目不会太难,但特别容易卡在一些莫名其妙的细节上,比如循环边界、数组越界、格式化输出。我当初刷到U12这个单元的时候,一共交了二十多次才把三道题全部一遍过,中间踩的坑到现在还记得很清楚。这篇文章就把U12第7~9题的完整思路、我自己的实现代码、还有调试过程中遇到的典型问题整理出来,给正在刷同一段题目的同学做个参考。
1. 动手之前:先看懂这几道题想考你什么
1.1 题号背后的学习线路
东华OJ的题目顺序一般是按照知识点推进的。前面的题大多围绕顺序结构、分支、单层循环展开,到28~30这个区间,基本进入数组和字符串处理的中等难度地带。U12对应的单元,从题目风格来看,重点集中在循环嵌套、数组下标操作、以及基础算法思维(比如枚举、模拟、简单排序或查找)。也就是说,这三道题不靠数学技巧取胜,更考验你对“数据在内存里如何存放、通过下标如何访问、循环什么时候该停”这些基本功的理解是否够扎实。
和后面那些动辄需要递归、链表、动态规划的题相比,这个阶段的题目属于“跳一跳够得着”的水平。你不需要提前学什么高阶数据结构,只要把教材上的数组、字符串相关内容吃透,大多数题都能靠硬模拟做出来。但恰恰因为不涉及高深算法,评测系统对边界条件的考察反而更严,漏掉一个特殊情况就可能整题WA。
1.2 建议的读题习惯和开发环境
我在刷这一段的时候,养成了一个读题习惯:先把输入输出样例复制到本地,然后用笔在纸上把样例对应的过程走一遍,搞清楚程序应该在哪个时刻输出什么,再动手写代码。这个方法听起来笨,但对解这类模拟题特别有效,能够提前把“输出格式”和“特殊情况”这两类最常见的坑找出来。
开发环境方面,我当时用的是Dev-C++,编译选项选C++17,但从头到尾写的都是面向过程的C风格代码。东华OJ对语言版本没有太苛刻的要求,C和C++都能提交,只是注意提交时主函数统一写成 int main(),结尾return 0,不要用void main这种不规范的写法。代码里尽量不用Windows特有的库函数,比如conio.h里的getch,评测环境不一定支持。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 约瑟夫环问题:循环和取模的经典组合拳
2.1 题目分析与常见解法
U12第7题,我印象里是一道约瑟夫环问题。题目大意是n个人围成一圈,从第一个人开始报数,报到m的人出列,然后下一个人重新从1开始报数,直到所有人都出列为止,要求输出出列顺序。n和m的取值一般不大,通常在100以内,所以不需要优化到数学公式的水平,直接模拟即可。
约瑟夫环的考点极其集中:第一是“环形”怎么用数组实现,第二是“跳过已经出列的人”怎么处理。很多人第一次做会想用链表,觉得这样删除方便。但对这个数据范围来说,链表反而容易在指针操作上出错,而且代码长、可读性差。用数组加标记法是最稳妥的方案:开一个数组记录每个人是否还在圈里,每次报数时跳过已经出列的人,报到m的人打上标记,输出编号。
c复制#include <stdio.h>
#include <string.h>
int main() {
int n, m;
int a[105];
scanf("%d %d", &n, &m);
memset(a, 0, sizeof(a)); // 0表示还在圈中,1表示已出列
int count = 0; // 已出列人数
int index = 0; // 当前下标
int num = 0; // 报数器
while (count < n) {
if (a[index] == 0) {
num++;
if (num == m) {
if (count > 0) printf(" ");
printf("%d", index + 1);
a[index] = 1;
count++;
num = 0;
}
}
index = (index + 1) % n;
}
printf("\n");
return 0;
}
核心就一行:index = (index + 1) % n。取模让下标在0到n-1之间循环,正好模拟了围成一圈的效果。报数器num只在当前位置的人还在圈里时才递增,已经出列的人直接跳过,但下标仍然要移动。这样等num累加到m时,当前index指向的人就是这一轮要出列的选手。
2.2 细节实现与常见陷阱
这段代码里有几个容易翻车的地方,我挨个说一下。
第一个坑是输出格式。OJ的题目经常要求每个数字之间用空格隔开,但行尾不能有多余空格。很多同学直接写成 printf("%d ", index + 1),然后发现Presentation Error,全是格式错误。我在第一次提交时就用了个flag变量控制空格,要么像上面代码那样用count判断是不是第一个输出,要么定义一个first标记。总之记住:OJ只认死理,差一个空格都算错。
第二个坑是数组下标的起点。如果题目说“从第一个人开始报数”,代码里index从0开始没问题。但有些变体题会说“从编号为k的人开始”,这时候index的初始值要相应改成k-1,不是死板地从0开始。这个变化很坏,但刷题笔记里值得单独记一笔。
第三个坑是n和m的边界。n=1时,循环一轮就完事,输出1即可;m=1时,每次报数立即出列,相当于按顺序输出所有人,这两种情况测试数据里一定会出现。我通常写完代码先手动测这两个边界,再测题目给的样例,最后才提交。
3. 字符串与ASCII码处理:看似简单实则套路多
3.1 题目分析与常见解法
U12第8题属于字符串处理,题目一般是要求对字符串里的字符做某种变换,比如大小写转换、加密解密、字符统计之类。东华OJ这个模块的题目风格很固定:给你的字符串长度不超过80或者100,操作逻辑一目了然,但必须注意字符和数字之间的转换。
这类题的通用解法是先将字符串读入,然后遍历每一个字符,根据题目要求判断它属于哪一类(大写字母、小写字母、数字、其他符号),再做相应处理。C语言里字符本质上是ASCII码,所以大写字母和小写字母之间的转换,不需要背ASCII表,直接利用字符常量之间的差值就能算出来。
举个例子,如果题目要求把大写字母变成小写字母,有人会写成 c = c + 32,也有人写成 c = c - 'A' + 'a'。两种写法都对,但我更推荐后一种,因为可读性强,而且不会因为记错差值而出错。
c复制#include <stdio.h>
#include <string.h>
int main() {
char s[105];
gets(s); // 注意:新版OJ可能要求用 fgets 代替 gets
int len = strlen(s);
for (int i = 0; i < len; i++) {
if (s[i] >= 'A' && s[i] <= 'Z') {
s[i] = s[i] - 'A' + 'a';
} else if (s[i] >= 'a' && s[i] <= 'z') {
s[i] = s[i] - 'a' + 'A';
}
}
printf("%s\n", s);
return 0;
}
3.2 实操实现步骤与易错点
这个题本身的算法难度不高,真正的坑集中在输入处理和字符范围判断上。
先说输入。gets函数曾经是很多教材的标准写法,但现在不少OJ的编译环境已经默认禁用gets,因为它存在缓冲区溢出的安全隐患。如果提交后报编译错误,提示gets was not declared,就要改用fgets。fgets的写法是 fgets(s, sizeof(s), stdin),它会连换行符一起读入,所以处理前要先把末尾的 '\n' 去掉,否则输出时会多一个换行。这个细节几乎每届刷题的人都会踩到。
再说字符范围判断。判断一个字符是不是数字,要用 s[i] >= '0' && s[i] <= '9',而不是 s[i] >= 0 && s[i] <= 9。我第一次写的时候就是忘了加引号,结果把字符变成了ASCII码比较,程序跑出来一堆乱码。同理,判断大小写字母时也要养成用字符常量做比较的习惯,既直观又不容易出错。
第三个容易忽略的地方是数组的长度。题目说字符串长度不超过100,有些人就开 char s[100],结果输入刚好长度100时,再加上末尾的 '\0',就已经越界了。正确做法是给数组多留一点余量,比如 char s[105] 或者 char s[110]。给数组留余量是刷OJ的通用好习惯,宁可多开几个字节,也不要去赌边界数据不会卡你。
4. 模拟计算题:一步步推导条件判断逻辑
4.1 题目分析与常见解法
U12第9题,我记得是一道带有条件的模拟计算题,大概是给定日期判断是一年中的第几天,或者是年份与月份交叉的日期逻辑题。这种题型的本质是“把自然语言的规则翻译成程序语言的判断条件”,考的是条件分支和循环的配合,以及能否把规则梳理清楚。
以日期计算为例,需要判断闰年、每个月有多少天、输入格式的解析,步骤比较固定。我的做法是先不急着写代码,而是把规则列出来:一年有12个月,其中1、3、5、7、8、10、12月有31天,4、6、9、11月有30天,2月平年28天、闰年29天。闰年的判断规则是:能被4整除但不能被100整除,或者能被400整除。把这些规则写清楚之后,代码就是翻译工作。
c复制#include <stdio.h>
int isLeap(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
int main() {
int year, month, day;
int daysOfMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int total = 0;
scanf("%d-%d-%d", &year, &month, &day);
if (isLeap(year)) {
daysOfMonth[2] = 29;
}
for (int i = 1; i < month; i++) {
total += daysOfMonth[i];
}
total += day;
printf("%d\n", total);
return 0;
}
用数组存每个月的天数,比用一堆switch-case简洁得多,这也是这类题目最推荐的写法。daysOfMonth[0]那个位置故意留空,是为了让下标从1开始对应从1月到12月,省去下标减一的心智负担。
4.2 边界条件与调试经验
这类模拟题最考验边界条件的完整度。我整理了一张自查清单,每当代码写完后都会逐项确认:
| 检查项 | 目标值/场景 | 预期结果 |
|---|---|---|
| 闰年2月29日 | 2024-02-29 | 第60天 |
| 平年2月28日 | 2023-02-28 | 第59天 |
| 跨月边界 | 2023-03-01 | 第60天 |
| 12月31日 | 2023-12-31 | 第365天 |
| 闰年12月31日 | 2024-12-31 | 第366天 |
| 无效日期 | 2023-02-29 | 需要先判断合法性 |
很多同学看到题目只要求输出第几天,就不去判断日期是否合法。但如果题目明文说了“输入保证是合法日期”,那确实不用判断;如果没说,最好从第一天开始就把合法性判断写上,避免评测数据里混入边界情况。判断方法很简单:day不能为0,不能超过对应月份的天数,month必须在1到12之间。
调试这类题目时,我习惯写一个临时输出语句,比如在循环内部打印当前月份累加的天数,确认累加逻辑是否符合预期。等确认无误后再删掉。用printf调试虽然笨,但对于新手来说,比凭空想代码逻辑有效得多。等调试工具用熟了,可以再用断点单步执行的方式,但在OJ场景下,printf调试法依然是最高效的排查手段。
5. 刷完这三题的通用经验:输出格式、数组越界和代码习惯
5.1 输出格式的“只有零次和无数次”
我刷OJ最大的感受是:代码逻辑在大部分情况下不是第一致命伤,输出格式才是。Presentation Error这种报错经常让人抓狂,明明输出内容都对,但就是多了或少了空格、换行。
解决方法只有一个:每次输出前先想清楚“行尾有没有空格”“最后一行要不要换行”“不同数据之间用什么分隔”。而且最好是写代码时就加对,不要等到提交后再靠评测结果来猜。我给自己定了一条规矩:凡是循环里要输出多个值,一律用count或first变量控制空格,绝不在printf字符串末尾写死空格。
5.2 数组越界排查:先开大,再定位
数组越界在OJ上是一种很难受的错误,它可能不报错,但结果随机错误,或者在某些数据下内存崩溃。排查思路分两步:第一步,把所有数组长度加5到10的余量,这一步能解决大多数“神秘错误”;第二步,如果加大数组后依然有问题,就需要回头检查循环里的下标运算,打印每个关键下标,看是否出现负数或者超出范围。
我在做这些题时,曾遇到过一次data[100]存不了长度100的字符串的情况,就是因为没考虑结尾的'\0'。后来我养成了一个习惯:所有数组大小 = 题目上限 + 5,这个“加五原则”看着粗糙,但确实能省掉很多无谓的WA。
5.3 代码备份和本地测试的重要性
这个阶段还有一个容易被忽视的问题:代码不备份。OJ平台虽然会保存历史提交记录,但如果你在本地改了代码想重交,本地文件和OJ上的版本对不上,调试起来非常混乱。我后面养成了习惯,每道题建一个单独的目录,文件名用题号命名,比如 U12_7.c、U12_8.c,每次修改前先复制一份旧版本。这个习惯在遇到“之前还能过,改了反而过不了”的情况时特别有用,能直接对比两个版本的差异,快速定位改坏了哪里。
6. 关于做题心态与后续建议
这三道题做完,基本可以确定一件事:你对数组、字符串和循环基础到底掌握到什么程度。如果一遍过,说明基本功不错;如果卡了很多次,也别灰心,这类题本身就是用来暴露问题的。我当年刷U12第8题的时候,先是gets编译报错,后来fgets把换行符带进去了,输出多了个空行,又改了一次才过。每一次报错都在告诉我一个具体的技术点没掌握好,这比直接看答案有用得多。
往后的题目会逐步引入排序算法、递归、链表、结构体等新内容,但如果现在就把数组下标、循环边界、字符串处理这些基础操作练扎实,后面学习新知识点会轻松很多。很多人觉得刷OJ靠天赋,其实对大部分人来说,拼的是能不能耐得住性子把细节一个个磨平。我的建议是,每完成一道题,就写几行注释记录这道题的“坑点”和“解法核心”,到复习时翻出来看一遍,比重新做一遍题都管用。
最后再分享一个小技巧:如果一道题怎么调都AC不了,而你的思路听起来没问题,可以尝试换一种实现方式重写整个代码,而不是在旧代码上不断打补丁。重写的过程中,思路会被迫重新梳理一遍,往往写着写着就发现之前忽略的漏洞了。我在U12这几道题上用过两次这招,效果都比盯着屏幕苦想要好。
