第一次在刷题平台上看到 L1-024“后天”这道 5 分题时,我差点就把它跳过去了。题面短到只有一句:输入一个 1 到 7 的整数表示今天是星期几,输出后天是星期几。当时我心想,这不就是 n 加 2 然后输出吗?结果提交上去,一个红色的 WA 让我愣了半天。翻车的根源不是题目藏了什么复杂陷阱,而是我把“星期”当成了一条直线,忽略了它是一个首尾相接的循环。
这篇内容就从这道题说起,把题目拆开、把取模公式的来龙去脉讲清楚,再聊聊这类周期性问题对后续刷题习惯的影响。无论你是刚接触 OJ 的新手,还是想回头整理基础题的老手,这 5 分里都有值得细看的东西。
1. 题目拆解:一个星期循环里的“后天”问题
1.1 题面回顾:短到一眼望穿的输入输出
L1-024 的完整描述,大概是这样:给定一个整数 n,取值范围是 1 到 7,1 表示星期一、2 表示星期二,一直到 7 表示星期日。要求输出“后天”是星期几。输入只有一行,输出也只有一个整数。分值标注为 5 分,属于最基础的一档题目。
看起来是道热身题,但第一次做的人很容易栽在“只看到了加法,没看到循环”上。星期一到后天是星期三,所以输出 n+2?这个直觉没毛病。问题出在星期这个系统的边界:星期六的后天是星期一,如果按 n+2 算出来是 8,这显然不在 1 到 7 的合法范围内。
也就是说,这道题真正要处理的是“越过边界之后如何回到起点”。顺着这个思路想下去,就自然引出了循环取模。
1.2 核心考点不是“加 2”,而是“循环取余”
星期是一个天然的循环结构,7 天一个周期,序列是 1,2,3,4,5,6,7,1,2,3,...。我们把问题抽象成数学语言:给定一个周期为 7 的序列,当前位置为 n,向后移 2 步,求新的位置。这正是模运算的经典入门场景。
最自然的写法是:
result = (n + 2) % 7这个式子在 n=5 时,5+2=7,7%7=0,得到“星期 0”——但星期编号里没有 0。问题出在哪?因为 1 到 7 的编号方案里,7 就是整除 7 的数,直接用 % 会把 7 映射成 0。解法是先整体平移 1 个位置,让数据落在 0 到 6 的区间,计算后再平移回来:
result = (n - 1 + 2) % 7 + 1简化一下就是:
result = (n + 1) % 7 + 1这个公式值得刻进脑子里,因为它是“带偏移量的循环取模”的标准写法。使用场景不只限于星期,任何从 1 开始的循环编号系统——月份、花色索引、槽位编号、轮询序号——都适用。
1.3 为什么一个 5 分题值得专门写一篇复盘
5 分题放在整场考试里几乎不影响大局,但它非常适合用来校准编程基本功。它一般只考察一个最小的编程闭环:读入、处理、输出。题目本身没有算法门槛,留给你的精力可以全部放在“处理”这一步的准确性和代码品味上。
很多人在简单题上眼高手低,直接套思路不测试,结果提交两次三次才过;这在一个界定如此清晰的题面里是不该出现的。认真对待它们,等同于在练“一次写对”的能力。L1-024 正是这样一个理想的训练样本:逻辑简单、边界明确、足够聚焦。
2. 解法演进:暴力分支、取模公式与查表法
2.1 暴力分支:思路最直白,但扩展性最差
我第一次 WA 之后,第一反应是写一堆 if-else:
#include <stdio.h> int main() { int n; scanf("%d", &n); if (n == 1) printf("3\n"); else if (n == 2) printf("4\n"); else if (n == 3) printf("5\n"); else if (n == 4) printf("6\n"); else if (n == 5) printf("7\n"); else if (n == 6) printf("1\n"); else printf("2\n"); return 0; }这段代码是对的,也最直白,一个分支对应一种结果,逻辑没有任何弯弯绕。但它有个致命问题——扩展性差。如果题目改成“十天后是星期几”,就要写 70 个分支;如果再改成一个动态偏移量,写法就彻底崩了。
暴力分支适合在不清楚取模公式时快速交卷,但不适合作为日常写法。它的真正价值在于帮你确认结论:答案集合确实是 3,4,5,6,7,1,2,这个序列本身就是“后天”在星期表上的映射。
2.2 取模公式:先平移再取模,避开“0 号星期”
既然答案序列已经确认,就可以尝试用一个公式覆盖所有情况。核心思路分三步:把星期编号对齐到 0 起点、执行加法、做模运算、再对齐回 1 起点。
#include <stdio.h> int main() { int n; scanf("%d", &n); printf("%d\n", (n + 1) % 7 + 1); return 0; }很多人会问,为什么不是(n+2)%7?原因前面提过:%7的结果范围是 0 到 6,会把星期 7 对应到 0。先减 1 再取模,取完再加 1,本质是从“1 索引”转换成“0 索引”再转换回来。
这个公式的价值在于,它让你直接把偏移量当作参数。明天是 +1,后天是 +2,三天后是 +3,只要改一个位置就行,甚至可以把偏移量改成变量,在运行时动态决定。实际上这已经是“任意天后是星期几”的通用解。
2.3 查表映射:星期推算场景下的另一种思路
第三种写法是查表:
#include <stdio.h> int main() { int n; scanf("%d", &n); int ans[7] = {3, 4, 5, 6, 7, 1, 2}; printf("%d\n", ans[n - 1]); return 0; }把答案序列提前算好,n 减 1 当作数组下标,一次取值就是结果。这段代码的可读性是最好的,完全没有“为什么加 1 再模 7”的理解负担。扩展方式也很直觉:偏移量变了,重新算一下表即可;规则比较复杂(比如工作日的排班表),直接改表内容就行,不用改逻辑。
查表法的缺点是要小心数组下标越界,以及表本身不能写错。n 范围是 1 到 7,n-1 正好是 0 到 6,不会越界;但如果输入边界没约束,表就随时可能悬空。OJ 题里输入条件通常给得很明确,查表法的风险可控。
2.4 三种解法横向对比
把三种写法放在一起看,会更容易理解各自的取舍:
| 解法 | 代码量 | 可读性 | 扩展性 | 易错点 |
|---|---|---|---|---|
| 暴力 if-else | 最长 | 直观但对新手啰嗦 | 差,偏移量一变就要重写 | 低,但容易漏分支 |
| 取模公式 | 最短 | 需要理解模运算语义 | 好,偏移量改参数即可 | 容易踩“星期 0” |
| 查表映射 | 中等 | 最好,数据和逻辑分离 | 中,改表比改代码简单 | 注意数组下标越界 |
5 分题的提交结果不会因为写法不同而改变,三种都能拿到满分。差别只体现在两点:一是你写代码时的思考负担,二是下次遇到同类问题时你能迁移多少经验。我个人会建议把三种都写一遍,不为了炫耀,而是让你在 5 分题里把“同一道题多种解法”的路径先跑通,后面遇到需要权衡的题才会有手感。
3. 提交验证中的边界值与细节陷阱
3.1 输入“7”时最容易翻车的深层原因
先讲一个很常见的翻车方式。很多同学写完代码后随手测几个输入:n=1 输出 3,n=2 输出 4,n=5 输出 7,看起来完全正确。提交之后 WA,百思不得其解。问题就出在没测 n=6 和 n=7 这两个边界值。n=7 时,n+2=9,直接输出 9,这是最典型的错法。
比直接输出 n+2 更隐蔽的错法是,有人会写成printf("%d\n", (n + 2) % 7)。n=6 时得到 1,结果正确;n=7 时得到 2,也正确;但 n=5 时得到 0,输出 0,错。这个 0 就是“星期 0 陷阱”。为什么 n=5 会错而 n=6、7 不会?因为 5+2=7,恰好整除 7。
正确的取模公式(n+1)%7+1在 n=5 时,6%7=6,+1 得 7;在 n=7 时,8%7=1,+1 得 2。全部边界都对。做完这一轮之后我才彻底理解:验证边界不是只验证输入的最大值和最小值,而是要验证“会导致结果跨越周期边界”的那几个输入,这个概念比这道题本身重要得多。
3.2 scanf 返回值与输入缓冲区的习惯问题
这道题的输入只有一个整数,很多同学会直接写scanf("%d", &n)而不看返回值。这在调试时没问题,但养成习惯后在更复杂的题里会埋雷。scanf会返回成功读取的参数个数,写scanf("%d", &n) == 1可以确认确实读到了数据。如果输入文件为空、格式异常,返回值不是 1,代码至少能提前退出而不是拿着未初始化的 n 往下跑。
还有一个容易忽略的细节是输入缓冲区。如果后面还要读字符或字符串,换行符会残留在缓冲区里,导致后续的getchar或gets读到空内容。这道题用不到,但练习时可以把scanf(" %d", &n)这种带前导空格的习惯建立起来,它会跳过空白字符,减少很多玄学问题。
3.3 同样一行代码,在 C 和 Python 里的取模差异
把同一道题用 Python 写出来,代码会更短:
n = int(input()) print((n + 1) % 7 + 1)看起来和 C 版一模一样,但底层语义并不完全相同。C 语言里%是取余,结果的符号和被除数一致;Python 里%是取模,结果的符号和除数一致,结果恒为非负。对于这道题,n 的有效范围是 1 到 7,两种语言的行为完全一致;但如果 n 被外部因素弄成负数或者超出范围,C 和 Python 会给出不同的结果。
这个差异在初级题里不会暴露,但是一个很好的提醒:跨语言移植公式时,不能只看表面写法,还要确认语言对取模和除法的底层约定。尤其在做日期、时间、金融计算时,负数取模的差异往往就是线上 bug 的来源。刷 5 分题时顺手验证一下这些行为,比专门去读语言规范要直观得多。
4. 一道 5 分题背后的周期思维与代码习惯
4.1 周期性问题抽象:从“后天”到轮询和约瑟夫环
“后天”是周期性问题里最简单的一种:一个编号系统,首尾相连,从当前位置移动若干步。把这个模型扩展开,你会发现它到处都有。
- 约瑟夫环:编号 1 到 n 排成一圈,每次数到 k 的人出列,下一次从出列者后面一位开始。圈上的“取模回绕”和“后天”同源,只是每次出列后集合在变。
- 进程轮询调度:时间片按顺序分配,用完最后一人回到第一人。
- 日期计算:已知今天是星期几,要算任意天数后的星期几,公式依然是
(base + days - 1) % 7 + 1。
这些场景的共同做法都是:先把自然编号转换成 0 起点的索引,再做运算,最后映射回自然编号。这一减一加看起来微不足道,却是容易出错的分水岭。在 L1-024 里把这个套路练熟了,后面看到类似的圈状、环状、周期问题,至少不会从零开始想。
4.2 可读性和简洁性的平衡:从一行流到具名函数
一行公式代码在 OJ 上很加分,但在真实工程里未必是好味道。如果你在代码评审时写出:
printf("%d\n", (n + 1) % 7 + 1);同事可能得看几秒才反应过来。更好的写法是给它一个名字:
int day_after(int today, int offset) { return (today - 1 + offset) % 7 + 1; }函数名本身就是注释,today、offset两个参数把意图表达得一清二楚。把“后天”理解为偏移量 2 的一次调用:
printf("%d\n", day_after(n, 2));代码量相近,语义却完全不同。这种把计算封装成可读单元的习惯,比背住一行公式重要得多。5 分题正是练习这个习惯的最佳场所,因为没有任何算法压力,可以专心打磨表达方式。
4.3 在入门题里训练对编译器细节的敏感度
另一个值得在简单题里做的事情,是主动测试编译器行为。比如把 n 改成 8,看输出是什么;把输入格式从%d改成%u,看输出有什么不同;把公式里的 7 写成 7.0,看类型转换会不会产生影响。这些实验在复杂题里不敢乱做,但在 5 分题里随便折腾都不会造成损失。
我特别喜欢拿这类题验证“类型陷阱”。C 语言的 int 和 unsigned 混用、除法向零取整、取模的符号规则,都是很多老手都会翻车的点。以 L1-024 这种题目为载体,用几十个测试输入去验证语言行为,所建立起的直觉会直接继承到后面更复杂的代码中。这不是浪费时间,这是用最低的成本换最扎实的底层感知。
5. 刷完这道题,我改掉的几个坏习惯
写到这里,说几个我自己的体会。
第一,提交之前我一定会在草稿纸上写下输入最小值、最大值,以及会导致周期回绕的中间值,比如 n=5 和 n=6 这种。L1-024 让我明白,边界测试不是最后补的步骤,而是编码前就该过一遍的工作。很多 WA 根本不是逻辑错,是边界没想清楚。
第二,我不再追求“第一时间写出最短代码”。先保证暴力解法能 AC,再回头优化成取模公式或查表,这样既不容易漏答案,又能比较不同写法的差异。5 分题的容错率很高,正好适合这种“先对、再美”的练习方式。
第三,我会特意把同一道题用 C 和 Python 各写一遍。语言差异在这种微型题里最显眼,也最容易理解。等到在工程里遇到需要跨语言协同的场景时,至少知道同一个公式在不同环境里可能会有不同的表现。
L1-024 只有 5 分,但从它身上拆出来的东西,绝对值不止 5 分。