洛谷P5707【深基2.例12】上学迟到,是我见过最能筛人的入门题之一。题面一句话就能看懂,完整C语言代码写下来不到20行,可它在洛谷入门题单里,WA率一直不低,很多新手反复提交都找不到原因。这道题真正考的并不是语法,而是把一个生活场景拆成数学表达式,再老老实实翻译成代码。下面就把“最晚几点起床”从读题到AC的完整链路拆给你看,适合刚刷完循环和分支、想用一道综合题检验自己的C语言初学者。
1. 先把题目“翻译”成人话:它到底在考什么
题目背景是这样的:主角从家出发去学校,家到学校的路程是 s 米,他走路的速度是 v 米/分钟,上课时间是早上8:00。这个人习惯提前10分钟到校。输入给出 s 和 v,要求输出他最晚几点起床。
这里有个点值得停下来想一下:题目说的是“提前10分钟到校”,不是“提前10分钟起床”。也就是说,10分钟是算在路上的总耗时里的,或者说到了学校之后要有10分钟缓冲。这一点理解错了,后面整个公式都会偏。
再看输入输出:
- 输入:一行两个整数 s, v
- 输出:一行 HH:MM
看到 HH:MM 就应该反应过来,题目对输出格式有严格要求:小时和分钟都必须两位,不足两位补前导0,比如 07:49,而不是 7:49。
1.1 题面与输入输出的真实含义
把题面里的关键信息对应到代码里的变量,是做题的第一步。我一般在草稿纸上列一张这样的对照表:
| 题面信息 | 含义 | 代码对应 |
|---|---|---|
| s 米 | 路程 | 输入整数 s |
| v 米/分钟 | 速度 | 输入整数 v |
| 上课时间 8:00 | 时间基准点 | 8 × 60 = 480 分钟 |
| 提前10分钟到校 | 额外缓冲 | 总耗时里再加10分钟 |
| HH:MM | 输出格式 | printf("%02d:%02d", h, m) |
把时间全部换算成分钟,是这类时间题最通用的做法。为什么不直接用 8 小时去减?因为后面涉及分钟和小时两个单位,分开算很容易出现进位、借位错误,比如“3点60分”这种根本不存在的时间格式。统一成分钟之后,所有计算都在整数范围内完成,最后再一次性拆成小时和分钟,思路会清晰很多。
1.2 为什么这道题是“入门但不简单”的典型
坦白说,这道题的语法难度几乎为零:只需要会 scanf、printf、if、整数除法。但它把好几个最容易在入门阶段被忽略的数学边界集中到了一起。
第一是除法方向。C语言的 / 对整数是向下取整,而题目要的是向上取整,这一点不搞清楚,样例可能侥幸过,换一组数据就WA。
第二是减法方向。从8:00往前推,如果耗时太长,减出来可能是负数,负数分钟怎么转成时间格式,是这道题真正考验逻辑的地方。
第三是格式化。很多新手代码逻辑全对,最终挂在 7:09 输出成 7:9 这种格式问题上。这些坑单拎出来每个都很小,但叠在一起,就是一道经典的“入门杀”题目。
2. 数学建模:从“最晚几点起床”到一条公式
2.1 先把时间统一成分钟
先定义两个基础量:
- 上学路上耗时:travel = 路程 / 速度(分钟,向上取整)
- 总耗时:total = travel + 10
然后起床时刻就是:
wake = 480 - total
这里 480 就是早上8:00的分钟表示:8 × 60 = 480。如果 wake 是正数,说明当天起床;如果 wake 是负数,说明从8点往前推不够推,得从前一天晚上开始算。
举个例子帮助理解。s = 1000,v = 100,travel = 1000 / 100 = 10,total = 10 + 10 = 20,wake = 480 - 20 = 460。460分钟 = 7小时40分钟,也就是 07:40。这个结果很直观。
但如果 s = 480,v = 1,travel = 480,total = 490。480 - 490 = -10,这是一个负数。这个负10分钟代表什么?代表从8:00倒退490分钟,已经退到前一天晚上去了。所以必须加上24小时,也就是1440分钟,变成 1430,再转成 23:50。这就是“跨天”处理。
2.2 向上取整:最容易漏掉的一环
题目里有一个隐含条件容易被经验不足的人忽略:速度和时间都是按分钟计算的,但路程除以速度并不一定能整除。比如 10 米的路,速度是 3 米/分钟,需要 10 ÷ 3 ≈ 3.33 分钟。可代码里根本没有小数分钟的概念,最小单位就是1分钟。你只走完了3分钟,还剩 0.33 分钟的路没走完,这0.33分钟也得算1分钟,所以耗时是4分钟。
这就是向上取整。C语言里 10 / 3 的结果是 3,如果直接拿这个 3 去用,就意味着少算了1分钟。在实际生活中只差1分钟可能没什么,但在题目里,少算1分钟就可能把起床时间算晚,导致迟到,所以这是致命错误。
向上取整的原理,可以理解成你约朋友8点见面,路上要走3分零1秒,你不可能3分钟就到,答案是4分钟。只要还有1秒没走完,就得按一整分钟算,这就是题目里时间的颗粒度。
那怎么在整数范围内做向上取整?两个办法:
- 判断能否整除:if (s % v == 0) travel = s / v; else travel = s / v + 1;
- 公式法:travel = (s + v - 1) / v;
第二种方法很多人不理解。它的原理是:整数除法向下取整,如果想向上取整,就在被除数上人为加一个“足够大但不至于影响整除部分”的数,也就是 v - 1。只要原式有余数,加上 v - 1 之后商就比原来多1;如果刚好整除,加上 v - 1 也不会让商发生变化。这个技巧在很多竞赛题里都很常用,建议直接记住。
2.3 跨天处理:起床时间可能在前一天晚上
wake = 480 - total,total 如果超过480,wake 就小于0。这时最晚起床时间其实落在前一天,需要加上 1440 把它拉回 0 到 23:59 这个合法区间。
新手常见的错误是直接输出负数,比如算出 -10,然后打印 -10 / 60 和 -10 % 60,得到类似 -0:-10 的奇怪结果。正确做法一定是在输出前判断:如果 wake < 0,就 wake += 1440。
严谨一点想:如果 total 特别大,wake 可能小于 -1440,原则上应该用 while 循环反复加 1440,直到结果非负。洛谷这道题的数据范围一般不会出现跨越两天以上的情况,用 if 加一次就够了。不过我在代码里更推荐写 while,逻辑更严谨,也不会多花任何成本。
3. 关键一步:向上取整、时间换算与跨天处理的C语言实现
3.1 向上取整的两种写法对比
先比较两种写法在实际使用中的区别。
写法一:if / else
int travel; if (s % v == 0) { travel = s / v; } else { travel = s / v + 1; }写法二:公式
int travel = (s + v - 1) / v;从可读性来说,写法一更直白,新手能一眼看懂“不能整除就多算1分钟”。从简洁性和防错角度来说,写法二更好,少了两行分支,也少了一种写错分支条件的可能。
我个人推荐写法二,但前提是你真的理解它为什么成立,而不是死记公式。如果不理解,建议先写写法一,确保逻辑正确,再尝试用公式替换。
3.2 从公式到代码:逐步翻译
把前面推导的公式翻译成C语言,核心流程只有四步:
- 读入 s 和 v;
- 算路程耗时 travel,注意向上取整;
- 算 wake = 480 - travel - 10,负数加1440;
- 用 %02d 格式化输出小时和分钟。
第3步里我用的是 480 - travel - 10,逻辑上等价于 480 - (travel + 10)。推荐写成 480 - travel - 10 这种展开形式,因为一眼就能看出“上课时间480减去路程耗时,再减去10分钟缓冲”。当然写成 480 - (travel + 10) 也行,看个人习惯。
这里还有几个容易被忽略的小地方。输入用scanf("%d %d", &s, &v)就够,竞赛环境下可以不检查返回值,但如果你想写成更健壮的样子,可以判断一下返回值是不是2,不是就提前结束。变量命名也值得注意,travel、total、wake 这种有明确含义的名字,会让排查问题轻松很多,别用 t、a 这种只有你自己看得懂的缩写。
3.3 格式化输出为什么用 %02d
到了输出这一步,仍然有坑。要求是 HH:MM,小时和分钟都占两位。7点49分要输出 07:49,不是 7:49。C语言里%d不会自动补前导0,写%02d才会:如果数值不足两位,左边补0;如果超过两位,照常输出。
需要注意顺序:先处理跨天,再输出。有些新手先把 wake 转成小时和分钟,再在负数状态下直接打印,结果既没补0也没跨天,整个输出完全错乱。正确流程是:判断并修正 wake,然后 h = wake / 60,m = wake % 60,最后 printf("%02d:%02d", h, m)。
4. 边界条件与隐藏陷阱:整除、补零、跨天、除零
4.1 几个必测的输入用例
把代码写完以后,强烈建议先用下面这几个用例自测,全过了再提交:
| 输入 | 期望输出 | 说明了什么 |
|---|---|---|
| 1 1 | 07:49 | 最短路径,验证基础计算 |
| 10 3 | 07:46 | 验证向上取整:10/3要按4分钟算 |
| 9 3 | 07:47 | 整除场景:3分钟整,不用+1 |
| 100 1 | 06:10 | 路程较长但仍在当天 |
| 480 1 | 23:50 | 验证跨天:总耗时超过480分钟 |
第2个用例最关键。如果代码用了普通整数除法 10/3,会算出13分钟总耗时,输出 07:47;但正确答案是14分钟总耗时,输出 07:46。这一个用例就能筛掉所有忘记向上取整的写法。
第5个用例用来验证跨天。s=480,v=1 时,走路要480分钟,加10分钟缓冲一共490分钟,8:00往前490分钟就是前一天23:50。如果你的代码看到 wake = -10 就不知所措,或者直接输出负数,这个用例会把你打回原形。
4.2 新手最常见的四类WA原因
我在一些新手交流区观察下来,这道题的WA基本集中在四类原因:
- 没用向上取整。直接用 s / v,被带小数的情况坑了。
- 忘了扣10分钟。“提前10分钟到校”这句话,很多人把它当成废话,计算结果比正确答案晚10分钟。
- 跨天只加一次或者干脆不加。要么输出负数,要么格式错乱。
- 输出格式错误。没写 %02d,输出 7:49 而不是 07:49。
前两类是逻辑错误,后两类是细节错误。但洛谷的评测不区分错误类型,只要输出跟标准答案不一致,就是WA。这也是为什么这道题看起来简单,却总有新手反复提交失败的原因——四个坑踩任何一个都过不了。
4.3 关于除零:v=0 时的处理思路
严格来说,这道题里速度 v 是正整数,题目数据不会出现0。但“除数不能为0”是C语言里最基本的常识,万一你拿着这道题的代码去改造成其他场景,或者自己扩展测试,碰到 v=0 时s / v会直接触发运行时错误,程序崩溃。
如果想把代码写得防御性强一点,可以在读入后加一个特判:
if (v == 0) { // 速度为0,题目不会允许,这里可以按无法到达处理 printf("00:00\n"); return 0; }不过竞赛环境下不需要这么写,因为题目保证 v > 0。我提这个,主要是想说清楚:整数除法的前提是分母不为0,实际开发中这个检查很便宜,值得养成习惯。
4.4 数据范围与 long long 的取舍
洛谷这道题的常规数据规模下,int 类型足够。32位 int 最大约21亿,而 s 和 v 通常是几万级别,travel 和 wake 的中间计算也远不会溢出。
但如果你刷到类似题目,发现数据范围可能达到 10^9 甚至更大,就要立刻想到用 long long。改起来也简单,把变量类型换成 long long,scanf 和 printf 的格式符换成 %lld,处理思路完全不变:
#include <stdio.h> int main(void) { long long s, v; scanf("%lld %lld", &s, &v); long long travel = (s + v - 1) / v; long long wake = 480 - travel - 10; while (wake < 0) { wake += 24 * 60; } printf("%02lld:%02lld\n", wake / 60, wake % 60); return 0; }养成“先瞄一眼数据范围再定类型”的习惯,能帮你避掉一整类溢出WA。
5. 完整代码与提交心得:如何做到提交一次过
5.1 可以直接提交的完整C语言代码
综合前面的分析,一份可以直接提交洛谷P5707的C语言代码如下:
#include <stdio.h> int main(void) { int s, v; scanf("%d %d", &s, &v); // 路程耗时,向上取整 int travel = (s + v - 1) / v; // 总耗时 = 走路时间 + 提前到校的10分钟 int total = travel + 10; // 上课时间按分钟计是 8 * 60 = 480 int wake = 480 - total; // 如果为负数,说明要从前一天晚上开始算,拉回24小时内 while (wake < 0) { wake += 24 * 60; } printf("%02d:%02d\n", wake / 60, wake % 60); return 0; }这份代码的核心就一行公式:(s + v - 1) / v,配合一个 while 做跨天修正,最后用%02d保证输出格式。整个程序没有任何高深语法,却把整数运算、分支循环、格式控制全部覆盖到了。
5.2 我在做题时坚持的三个习惯
最后分享三个我自己的小习惯,不仅是这道题,刷所有洛谷入门题都适用。
第一个习惯:先手算样例,再写代码。拿到题目先在草稿纸上把公式列出来,用题目给的样例手算一遍,确认结果与题目输出一致,再动手。这样能避免“代码写完了才发现思路从一开始就错了”的浪费。
第二个习惯:提交前自定义三个边界用例。一个最小输入、一个整除边界、一个跨天用例。这道题就是 (1,1)、(9,3)、(480,1),三组数据一测,四类WA原因当场就能发现。
第三个习惯:WA 之后先看数据,别看题解。洛谷的评测会给出错误用例,把错误用例带进本地程序跑一遍,用 printf 把 travel、total、wake 中间值都打出来,一般一眼就能定位问题。实在看不出来再上调试器。看题解是最后的手段,因为自己排查一遍,比看十次题解都记得牢。
另外,如果你偶尔遇到“提交失败无法解析路由对象”之类的提示,先别慌。这是网站服务端的偶发问题,和你的代码无关,稍后重新提交就好,不要在本地反复改代码浪费时间。
5.3 一个值得做的扩展练习
最后再啰嗦一句:这道题的价值不在代码量,而在它强迫你想清楚“整数除法的方向”“跨天要怎么折算”“输出格式如何保证”这三件事。想明白这三个点,后面再遇到日期时间类的题,比如更复杂的时钟问题、日历问题,思路都会顺很多。
你可以自己把题目改一版练手:如果上学时间是 8:10,或者要求提前15分钟到校,公式该怎么调整?如果输入给的是起床时间,让你判断会不会迟到,反过来怎么比较?想通这些扩展,P5707 才算真正刷透了。