在洛谷刷题列表里,P1424《小鱼的航程(改进版)》属于那种“看着人畜无害、代码却容易写歪”的入门题。题目只说了一件事:小鱼工作日每天游250公里,周末休息,从周x开始算起,过了n天后一共游了多少公里。输入两个整数x、n,输出一个整数。很多人第一次AC之前,都会在“从周x开始”这个表述上栽跟头——它到底算不算第1天?起点是周末又该怎么办?这篇就来完整拆一遍这道题,包括题意陷阱、两种主流写法、边界测试,以及从里面提炼出来的一套周期类问题通用解法。不管你是刚学循环的新手,还是已经能轻松AC入门题、想升级思维的老手,这篇文章都值得花十分钟读一下。
1. 为什么说这题是“周期问题”的入门代表作
1.1 题面拆解与两个关键设定
先看原题描述:有一只小鱼,平日每天游泳250公里,周末休息(实行双休日),假设从周x(1≤x≤7)开始算起,过了n(n≤10^6)天以后,小鱼一共累计游泳了多少公里?
输入很简洁,就两个整数x和n,x表示起始日是星期几,1是周一,7是周日,n表示持续多少天。输出一个整数,累积游泳里程。
这个题的核心考点有三个:
- 周期识别:7天为一个完整循环,工作日和休息日有固定规律。
- 起始偏移:不是从周一开始,而是从给定的工作日或周末开始。
- 取模运算:星期几在推进时的循环跳转,本质上是“模7”运算。
“改进版”三个字其实很有信息量。我的理解是,题目想强调的已经不只是“会不会写for循环”,而是“能不能在周期问题上做优化”。原版可能允许纯暴力模拟,但改进版把n扩大到10^6,并且用“从周x开始”这个设计故意制造偏移量,逼着你去思考比逐天模拟更高效的路径。
这类问题在算法竞赛里有个专属分类,叫“周期/日期模拟”。它不涉及高深的数据结构,也不要求套模板,但非常考验对取模和偏移的理解。很多选手在后续做“给定某天是星期几,问n天后是星期几”以及“两个日期之间有多少个工作日”这类题时,都会回头用到这套思路。
1.2 从样例反推“过了n天”的确切含义
题目给了一个样例:输入3 10,输出2000。我们可以用手算验证一下,顺便把“从周x算起,过了n天”这句话的真实含义彻底搞清楚。
假设从周三开始,10天分别是:
- 第1天(周三):游250公里
- 第2天(周四):游250公里
- 第3天(周五):游250公里
- 第4天(周六):休息
- 第5天(周日):休息
- 第6天(周一):游250公里
- 第7天(周二):游250公里
- 第8天(周三):游250公里
- 第9天(周四):游250公里
- 第10天(周五):游250公里
数一下工作日:8天。8×250=2000公里,和样例输出完全一致。
这个反推非常关键,它确认了两件事:第一,起始日周x算作第1天,不是从下一天开始;第二,周末休息就是纯粹的周六、周日两天,不游泳。很多初学者会在这里踩坑,把“从周x算起,过了n天”理解成“从周x的明天开始算n天”,这样样例算出来就是7个工作日,1750公里,直接错掉。
所以读题时一定要动手推一遍样例,不要觉得样例只是拿来验证答案的。推演样例的过程,本质上是把文字题面翻译成精确的数学模型的过�。这道题能不能一次AC,很大程度上取决于这一步有没有做扎实。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 按天模拟:先跑通,再优化
2.1 最直接的实现:循环n次,逐天判断
如果你刚学编程不久,看到“过了n天”这种描述,第一反应肯定是循环n次,每天判断一下是不是周末,不是周末就加250公里。这是完全正确的思路,也是我推荐新手先写出来的版本。
cpp复制#include <iostream>
using namespace std;
int main() {
int x, n;
cin >> x >> n;
int ans = 0;
for (int i = 0; i < n; i++) {
if (x <= 5) {
ans += 250;
}
x = x % 7 + 1; // 更新到下一天
}
cout << ans << endl;
return 0;
}
这里的核心逻辑就两句话:
x <= 5判断当天是不是工作日,因为周一(1)到周五(5)都是工作日,周六(6)和周日(7)休息。x = x % 7 + 1把星期推进到下一天。这个公式是周期跳转的关键,它保证:1→2,2→3,……,6→7,7→1。单独看(x + 1) % 7在x=7时会变成0,而0不是合法的星期表示,所以必须先减1取模再加1,或者用x = (x == 7 ? 1 : x + 1)这样的判断,但显然x % 7 + 1最简洁。
很多新手会疑惑:为什么不能直接写x++然后判断?因为7之后会变成8,还需要额外处理回1。x % 7 + 1把判断消掉了,一次取模一次加法,干净利落。
2.2 为什么模拟解法在本题也能AC
按天模拟的时间复杂度是O(n),n最大是10^6。在C++里跑10^6次循环,通常在1毫秒上下,哪怕算上输入输出的开销,也完全在评测系统的时间限制内。所以这道题你用模拟写法提交,一样能AC。
但你要清楚它为什么能AC:不是因为你代码写得优雅,而是因为数据范围恰好允许。如果把n改成10^9甚至10^18,这个解法瞬间就超时了。这也是我把“先跑通”放在前面的原因——入门阶段,正确性优先,先写出一份能过的代码建立起信心,再去琢磨更优的做法,学习曲线会更平滑。
但是,模拟写法有一个隐藏风险:它把答案的计算隐藏在了循环里,如果你哪天忘了更新x,或者把x <= 5写成x < 5,调试起来会比较痛苦。更重要的是,逐天判断的思路很难迁移到更复杂的周期问题里。比如题目变成“每周第2天和第5天休息,问n天后游了多少”,模拟代码的if条件会变得混乱;而用数学方式拆解后,只需要改一处周期贡献的计算。
3. 更漂亮的O(1)解法:先拆整周,再处理零头
3.1 关键推导:完整周的天数直接算
7天是一个完整周期,其中周一到周五游,周六周日休息。所以每一完整周里,小鱼游了5天,共5×250=1250公里。
我们不需要一格格数,可以直接用整除算出有多少个完整周:
week = n / 7
这week个完整周贡献的里程数就是:
week * 5 * 250
也就是week * 1250。先把这段加上,问题就只剩下“零头”了。
零头天数是多少?用n % 7得到。比如n=10,完整周是1周,零头是3天。这3天里到底有几天是工作日,取决于起点星期x。
3.2 如何处理“从周x开始”的零头天数
处理零头天数的核心是:从x开始,连续rest天,数出其中有多少个工作日。这里最容易被绕晕的就是偏移计算。
正确做法是把星期几统一映射成0到6的索引。假设我们用0代表周一,1代表周二,……,6代表周日,那么起始日x对应的索引是x - 1。从起始日开始的第i天,索引就是(x - 1 + i) % 7。如果这个索引小于5,说明是周一到周五中的某一天,应该游泳。
写成代码就是:
cpp复制for (int i = 0; i < rest; i++) {
int today = (x - 1 + i) % 7 + 1; // 转回1~7表示星期
if (today <= 5) {
ans += 250;
}
}
我特别想强调一个很多初学者会犯的错误:有人会图省事写成(x + i) % 7 + 1,觉得差不多。但x=1时,(1 + 0) % 7 + 1 = 2,把周一变成了周二,整体偏移了一天;x=6时,(6 + 0) % 7 + 1 = 0,直接把星期变成了非法值。所以这里必须先减1再取模再加1,三步缺一不可。
如果想完全避开循环,还能继续用数学方式直接数出rest天里的周末数量,但没必要。因为rest是小于7的余数,循环最多执行6次,常数级别,已经是O(1)了。
3.3 C++和Python的实现与复杂度
完整的C++解法:
cpp复制#include <iostream>
using namespace std;
int main() {
int x, n;
cin >> x >> n;
long long ans = 0;
int week = n / 7;
int rest = n % 7;
ans = 1LL * week * 5 * 250;
for (int i = 0; i < rest; i++) {
int today = (x - 1 + i) % 7 + 1;
if (today <= 5) {
ans += 250;
}
}
cout << ans << endl;
return 0;
}
Python版本更简洁:
python复制x, n = map(int, input().split())
ans = (n // 7) * 5 * 250
for i in range(n % 7):
today = (x - 1 + i) % 7 + 1
if today <= 5:
ans += 250
print(ans)
这个解法的时间复杂度是O(1),因为循环最多执行6次,无论n多大都只和余数有关。空间复杂度O(1)。相比按天模拟,它直观地把“整周贡献”和“零头校正”分开处理,逻辑清晰,也不容易写错。
我建议每个做完这题的人,都认真把这个数学版本敲一遍。哪怕你已经AC了模拟写法,也值得把代码重构一次。重构的过程就是建立“周期思维”的过程。
4. 边界用例与踩坑复盘
4.1 一套能筛掉90%错误的手算用例
我在本地测试这类题目时,习惯用表格维护一组手算边界用例,每次写完代码直接跑。下面这几组基本覆盖了所有容易错的场景:
| 输入 (x, n) | 期望输出 | 手算说明 |
|---|---|---|
| 1, 1 | 250 | 从周一开始,1天工作日,250 |
| 1, 7 | 1250 | 周一到周五游5天,周六周日休 |
| 5, 3 | 250 | 周五游,周六周日休,只游1天 |
| 6, 1 | 0 | 从周六开始,直接休息 |
| 6, 2 | 0 | 周六周日连续休息 |
| 7, 3 | 500 | 周日休,周一、周二游 |
| 3, 10 | 2000 | 题目样例 |
这里特别容易出问题的是7, 3。很多人习惯性认为“n=3就是3天都游”,忽略了起点是周日。用7, 3测试,能立刻暴露你是否正确处理了起点为周末的情况。
另外5, 3也很考验人:周五游完正好接周末,如果代码里用(x + i)这种偏移错误,很可能在边界上算成2天甚至3天工作日。这两组数据建议刻进脑子里。
4.2 新手最容易踩的五个坑
我根据带新人的经验,把这道题的常见错误总结成五类,每一类都有明确的解决方向:
-
起始日算错了。把“从周x开始”当成“下一天开始”,导致所有结果偏小。解决方法是手动推一遍样例,确认起始日算第1天。
-
取模公式写错。
(x + 1) % 7在x=7时会得到0;(x + i) % 7 + 1在x=1时会偏移一天。统一用(x - 1 + i) % 7 + 1,并按0到6的索引思维去理解。 -
数学解法里忘记加整周贡献。有人只处理了
n % 7的零头,把前几周的1250公里漏了。你算出来的答案会明显偏小,并且n越大越离谱。 -
把周末判断写成
x != 6 || x != 7。这是一个经典逻辑错误,它的结果是恒为真,因为x不可能同时等于6和7。要用x != 6 && x != 7,或者更直白的x <= 5。 -
输入顺序读反。题目先给x再给n,但有人受“n天之后”的语感影响,先读n再读x,导致结果完全不对。这个毛病在考试里特别冤,建议提交前先核对一遍输入顺序。
4.3 数据类型的提醒
本题n最大10^6,理论上最大里程是250×10^6=2.5×10^8,在int范围内。但数学解法里如果先算n * 250再乘5,虽然数值上没问题,可一旦题目数据加强,比如n变成10^9,int就会溢出。
我个人的习惯是:涉及乘法且数据范围不明确时,直接用long long,成本极低,但能省去很多莫名其妙的Wrong Answer。C++里尤其注意,week * 5 * 250中的week本身是int,直接相乘在int域运算,如果想强迫它用long long,用1LL * week * 5 * 250来定式转一下,这个写法值得养成习惯。
5. 从P1424抽出来的“周期题通用套路”
5.1 一套可以迁移到其他题的思考框架
P1424虽然简单,但它完整覆盖了周期类问题的所有核心步骤。我从这道题里提炼出一个“四步拆解法”,做同类题时直接套用:
- 第一步:找周期长度。本题是7天一个周期,其他题可能是“每3天”“每10天”或者“一个月30天”的循环。
- 第二步:算单个周期的贡献。本题是5个工作日×250=1250公里。
- 第三步:用整除拆出完整周期,把完整周期的总贡献先算出来。
n / 周期长度就是周期个数。 - 第四步:用余数处理不完整的“零头”。这时从起点开始逐项枚举,因为余数一定小于周期长度,枚举次数极低,不会影响复杂度。
这套框架的威力在于,它把“逐天模拟”变成了“批量计算+小规模校验”。一旦你的数据规模变大,或者时间限制变严,你会庆幸自己当初没有只满足于跑通模拟解法。
重点提示:第四步里,起点的“偏移量”是几乎所有周期类问题的命门。起点不在周期首部时,必须先把起点映射成0-based索引,这里对应到代码就是x - 1。很多题目故意把起点设计成“不友好的位置”,比如从周六开始、从每月15号开始,都是为了测试你是否有偏移意识。
5.2 两个变式练习
如果你想验证自己是不是真的掌握了这套思路,可以做下面两个变式,都是P1424的简单改造:
变式一:n变成10^18。原题n是10^6,如果你用模拟写法,在这个数据范围下必然超时。用“拆整周+余数枚举”的写法,把数据类型改成long long,答案依然秒出。这个变式不需要额外规则,只是为了逼你确认自己写的是O(1)而不是O(n)。
变式二:把“周末休息”改成“每周的周三和周五休息,其余天数每天游300公里”。周期还是7天,但单个周期的贡献从“5天×250”变成了“5天×300”,并且零头天数的判断条件要从today <= 5改成today不是3且不是5。
变式二很多人会下意识继续用today <= 5来判断工作日,结果就在周三那个点出错。这种“改条件”的练习,能帮你把“周期”和“工作日/休息日”彻底解耦。以后遇到再变态的休息规则,你也能条件判断上一行就改完。
6. 写在后面:简单题的价值在哪里
这些年我见过太多刚开始刷题的人,AC了就秒切下一题,觉得这种入门题不值得复盘。但实际上,真正影响你后续水平的,恰恰是能不能从简单题里提炼出可迁移的套路。P1424就是一块很好的磨刀石:它没有复杂的算法,却包含了取模、偏移、整除、边界测试这些以后天天用到的基本功。
我自己的习惯是,每题AC之后多问自己一句:如果n再大十倍,我的解法还成立吗?如果不成立,那有没有更优的数学写法?这个问题会逼着你去思考题目的本质,而不是只满足于“过了”。这道题我前前后后写过模拟、整周拆分、纯数学三种版本,每次重写都有新的理解,尤其是对“为什么统一用(x - 1 + i) % 7 + 1”这个细节,是在写了很多次之后才真正形成肌肉记忆的。
最后分享一个小技巧:做题时把“星期几”统一看成0到6的索引,而不是1到7。虽然题目输入输出都是1到7,但在中间计算时,0-based能让取模运算天然正确,不需要额外处理那些“+1再-1”的边界。等你哪天开始做复杂的日期计算题,就会知道这个习惯有多香。刷题这件事,很多时候就是一通则百通,希望这篇复盘能帮你把这道“小鱼的航程”吃透。
