
做编程机试刷题的朋友肯定绕不过牛客网 KY 题单“KY257 日期累加”是我刷题时印象很深的一道题。题目描述不长给你一个起始日期和一个累加天数输出累加后的日期样例日期可能落在闰年边界累加天数可能大到需要跨好几年甚至十几年。但它背后考的东西一点也不少闰年判断、大小月天数、跨年进位、格式化输出每一项都是日期类问题里的“基本功”。把这题吃透后面遇到日期差值、星期推算、日志时间处理思路都能顺下来。这道题适合三类人准备考研机试或校招笔试的学生、刚入门算法刷题想找点“有手感”题目的新手、还有平时写脚本做计划排期被日期处理折磨过的开发者。下面我把自己完整做题的思路、两种可选方案、一份能直接跑的代码以及调试时踩过的坑一次讲清楚。1. 拿到题目别急着敲代码KY257 到底在考什么1.1 题目描述与核心需求拆解题目一般会这样给第一行输入一个整数 n表示有 n 组测试数据。之后每组数据一行包含四个整数 y、m、d、numy 表示年份m 表示月份d 表示日期num 表示要累加的天数。要求对每一组数据输出从 y 年 m 月 d 日往后数 num 天之后的日期格式是 yyyy-mm-dd年、月、日都要补齐到两位以上。比如说输入2024 2 28 1输出的应该是2024-02-29因为 2024 年是闰年2 月有 29 天。但如果输入2023 2 28 1输出就是2023-03-012 月只有 28 天。看起来就是一个“给日期做加法”的操作可实际操作起来你要处理的不是十进制加法而是几套规则混在一起的“混合进制”月份是 12 进制天数是变进制2 月的天数还随着闰年变化。拆开看这题的核心需求只有一句话在合法日期输入的前提下正确处理天数溢出时的进位问题。天数超过当月最大值就要进位到月份月份超过 12就要进位到年份年份进位后又可能碰到新的闰年规则进而影响下一年 2 月的天数。如果把这一串逻辑捋顺了代码其实很短捋不顺很容易写出“看起来对一到边界就挂”的版本。1.2 为什么这道题值得练三个隐藏考点我刷题有个习惯看到这种基础题不会直接跳过而是会想它到底在“筛选”什么能力。KY257 至少藏了三个考点第一是闰年规则是否真的理解。很多人背了“四年一闰百年不闰四百年再闰”这句话但写代码时经常只写year % 4 0把整百年这个特殊情况漏掉。1900 年不是闰年2000 年是闰年这两个年份放在一起测试马上就能暴露问题。第二是月份天数表怎么组织。用一个长度为 13 的数组下标 1 到 12 对应 1 月到 12 月是日期类问题里最常用的做法。第 0 位空出来目的是让下标和月份一一对应写起来直观不容易错。别小看这种习惯在机试那种紧张环境里少一个“下标减一”的思考步骤就是少一个出错机会。第三是进位逻辑的完整性。月份到 12 月之后要继续进位很多人只记得m忘了判断m 12的时候要把年份加一、月份重置为 1。还有一个细节每次从当月天数中减掉多少取决于“当前月份”的天数而当前月份在循环里是会变的。如果写成固定用一个数组值去减跨月之后就会出问题。2. 算法思路与方案选型从逐日模拟到快速进位2.1 三种方案对比逐日循环、按月进位、转绝对天数拿到日期题我脑子里一般会闪过三套方案。第一套是逐日循环把 num 当成一个计数器一天一天往后走每走一天更新一次日、月、年。这种方案逻辑最简单几乎是“翻译题目”但如果 num 很大比如 10 的 9 次方循环一亿次机试肯定超时。它适合用来验证思路不适合当最终提交版本。第二套是按月进位这也是我推荐给大多数人的方案。核心做法是先把要累加的天数加到当前日期的“天”上得到一个总天数然后只要这个总天数大于当前月份的总天数就用它减去当月天数同时月份加一月份超过 12 就年份加一月份回到 1。这个方案循环次数很少num 就算有 10 的 6 次方最多也就循环几年到几十年性能完全够用。代码写起来也直观几乎不会出现“逻辑绕圈”的问题。第三套是转绝对天数把输入的日期换算成从某个基准日期开始的总天数加完 num 后再换算回年、月、日。这套方案在日期差值类题目里非常好用因为它把“日期差”变成了“整数差”很多复杂问题瞬间变成普通整数运算。缺点是需要先写两个换算函数稍微多一点代码而且换算公式一旦记错整个结果都会偏。对于 KY257 这种“只往后加不往前减”的题没必要用这么重的工具。我自己的建议是机试用按月进位项目里频繁做日期运算再用绝对天数。这两套方案未来都会用到但刷题时先用简单的把题拿下不要上来就整复杂方案。2.2 闰年规则与月份天数表为什么不是每个 2 月都有 29 天闰年的定义是能被 4 整除但不能被 100 整除的年份或者能被 400 整除的年份是闰年。写成表达式就是bool isLeap(int y) { return (y % 4 0 y % 100 ! 0) || (y % 400 0); }这个规则跟“四年一闰”的直觉不一样很多人问过为什么 1900 年能被 4 整除却不是闰年。简单说地球绕太阳一周大约是 365.2422 天如果每 4 年加一天平均每年会多出大约 0.0078 天积累几百年误差就大了。所以历法上规定整百年必须能被 400 整除才算闰年通过“少闰三次”来把误差抹平。作为写代码的人不必去背天文历法的推导但需要知道这个规则的最终形态就是上面的逻辑表达式。月份天数表则是日期问题的“基础设施”。除了 2 月随着闰年变化其他月份的天数是固定的1、3、5、7、8、10、12 月都有 31 天4、6、9、11 月都是 30 天。把它们存进数组之后查表就行int daysInMonth(int y, int m) { int days[13] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (m 2 isLeap(y)) { return 29; } return days[m]; }注意数组第 0 位我故意放了一个 0这样days[1]就是 1 月天数days[12]就是 12 月天数。如果从 0 开始存每次都要m - 1不仅多写代码还容易在取数时搞混。2.3 按月进位为什么能避免“性能翻车”有些人觉得“反正机试数据量不大我直接 day 循环也没问题”。我见过不少这样的代码测试用例少的时候确实能过可一旦题目把 num 放大到百万级别循环里还要反复调用闰年判断函数运行时间就会很难看。按月进位的本质是“能减一个月就减一个月”直接把天数除以当月天数的整数部分“压缩”成月份增量。举个例子2024-01-31累加 100000 天如果逐日循环要跑十万次按月进位循环的次数等于跨过的月份数最多也就几十次。之后再处理剩下的小于一个月的零头一次 if 判断就能解决。还有一点很多人没意识到逐日循环如果写得不好很容易在“月末最后一天”这种位置产生重复判断。比如 1 月 31 日加 1 天要先判断当前是不是 1 月再判断是不是 31 日再处理进位2 月 28 日加 1 天又得先判断是不是闰年。这些分支组合起来代码会膨胀得很厉害。而按月进位把“天”直接相加再统一判断溢出分支数量少很多调试起来也更轻松。3. 可复现的标准实现核心代码与逐行解析3.1 完整可运行的 C/C 代码下面这份代码我按机试标准写的兼容 C 和 C 的编译环境输入输出用scanf和printf稳定性优先#include cstdio bool isLeap(int y) { return (y % 4 0 y % 100 ! 0) || (y % 400 0); } int daysInMonth(int y, int m) { int days[13] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (m 2 isLeap(y)) { return 29; } return days[m]; } int main() { int n; scanf(%d, n); while (n--) { int y, m, d, num; scanf(%d %d %d %d, y, m, d, num); d num; while (d daysInMonth(y, m)) { d - daysInMonth(y, m); m; if (m 12) { m 1; y; } } printf(%04d-%02d-%02d\n, y, m, d); } return 0; }如果你用的是 Java 或 Python思路完全一样只是语法不同。重点不是语言而是“先把天数加到 d 上再循环借位”这一步。3.2 关键代码段为什么这么写最核心的循环就是这三行while (d daysInMonth(y, m)) { d - daysInMonth(y, m); m; if (m 12) { m 1; y; } }我解释一下这三行背后的逻辑。第一步d num把累加天数直接加到日期字段 d 上此时 d 可能已经超过当月天数但先不管交给后面的循环去处理。第二步只要 d 还大于“当前月份的天数”就用 d 减去这个天数同时月份加一。这里有个很容易被忽略的细节减去的天数必须是“当前月份”的天数而不是最开始那个月份的天数。因为这个循环每执行一次m 都可能变化所以要反复调用daysInMonth(y, m)去取最新的结果。第三步是月份溢出处理。m 是一个 1 到 12 之间的整数当m之后变成 13说明已经跨过年末这时候把 m 重置为 1年份加一。这里为什么是“先加 m 再判断”因为 12 月的天数减完之后自然就应该进入下一年的 1 月如果把判断写在m之前代码会复杂很多。还有一点值得学习while (n--)这种写法在 n 等于 0 的时候会直接退出循环非常干净。如果你用for (int i 0; i n; i)也没问题但 while 写法在机试时代码更短。3.3 输入输出格式格式化补零的坑题目要求输出yyyy-mm-dd也就是年份至少四位、月份和日期都是两位。C 语言里用printf(%04d-%02d-%02d\n, y, m, d);就能搞定。%04d的意思是如果年份不足四位用 0 补齐到四位如果超过四位就原样输出。比如 9999 年再加一年变成 10000%04d输出的仍然是10000不会截断。有些考生会自己写补零逻辑比如if (m 10) printf(-0%d, m); else printf(-%d, m);这样做没错但机试时间宝贵直接用格式化符号更省事。记得用%02d而不是%2d。%2d是右对齐前面补空格输出会变成 1而不是01判题会直接判错。这个问题我在帮学弟调代码时见过很多次属于“不是不会而是没记住格式符号含义”的冤枉分。另外如果你用的是 JavaSystem.out.printf(%04d-%02d-%02d%n, y, m, d);是等价写法Python 可以用print(f{y:04d}-{m:02d}-{d:02d})。核心都一样补零必须是“左补零”不是“右补零”。4. 用边界案例验证正确性踩过的坑全复盘4.1 闰年二月前后的经典三连测写完代码别急着交先在本地跑几个边界用例。我最常用的一个测试组合是闰年的 2 月 28 日、2 月 29 日和平年的 2 月 28 日输入日期累加天数预期输出备注2024-02-2812024-02-292024 是闰年2 月有 29 天2024-02-2912024-03-01闰年 2 月最后一天2023-02-2812023-03-012023 是平年2 月只有 28 天1900-02-2811900-03-011900 不是闰年这里是易错点2000-02-293662001-02-282000 是闰年跨越完整一年后回到平年 2 月末这几个用例的意义在哪它能把闰年判断、二月天数、跨年进位三个问题同时暴露出来。比如把闰年条件写成y % 4 0的人在 1900-02-28 这个用例上会输出1900-02-29直接暴露错误。把月份天数表第 2 月写成固定 28 天的人在 2024-02-28 和 2024-02-29 这两个用例上会“恰好”对一半但 2024-02-29 加 1 天就会错成 2024-03-02因为程序把不存在的 2 月 30 日也“过”了一遍。所以这三连测的覆盖力非常强。2000-02-29 加 366 天这个组合是我特意加的它验证的不只是当月边界还有“跨过一个闰年后再落入平年”的场景。如果你直接把 366 当成“一年”去处理很可能算成 2001-03-01那就错了。正确的进位方式是一天一天进位或者一个月一个月进位让闰年的 2 月 29 日自然地被“消耗”掉。4.2 跨年与跨闰年的复杂场景跨年场景最典型的是年末最后一天加 1 天2023-12-31 1 2024-01-01。多数人能想到这里但很少能想到连续跨多个年末的用例。比如2023-11-30 100 2024-03-09这中间要经历 12 月、1 月、2 月其中 2 月是闰月的平年状态一个不小心就会漏掉 2 月的天数变化。我还喜欢测试一个“跨四年周期”的用例2023-01-01 1500。2023 年剩余天数是 3652024 是闰年有 366 天2025 和 2026 各 365 天。你可以不手动算精确答案而是先用代码跑一遍再用 Python 的datetime模块或者在线日期工具验证。只要这种长跨度用例输出正确说明代码对“年份变化导致 2 月变化”的处理是动态的而不是缓存了某个固定值。调试这类用例的时候我有个习惯把循环里的中间状态打印出来。比如在while循环里临时加一行printf(y%d m%d d%d\n, y, m, d);。这样可以看到每一天或每一月是怎么进位的。有一次我发现输出结果差一天仔细看中间状态才发现我在月份溢出后忘记把 d 重新初始化导致 1 月会少算几天。肉眼盯着代码很难发现这种状态依赖 bug打印中间状态就一目了然。4.3 大天数累加与性能边界如果题目把 num 给到 100000 甚至 1000000按月进位依然能轻松跑完。但逐日循环的版本可能已经开始卡顿。我实际测试过在普通笔记本上逐日循环 10 万次没什么感觉但到 1000 万次就会明显停顿如果机试的时限是 1 秒再加上多组测试数据超时风险很大。为了验证按月进位的性能我构造过一个大用例1 1 1 1000000000也就是从公元 1 年 1 月 1 日累加 10 亿天。这个用例下按月进位代码的循环次数大约是跨过的月份数大概在 300 多万次左右因为 10 亿天除以平均每月 30 天就是 3000 多万个月不对10 亿天约等于 270 多万年让我重新算一年约 365 天10 亿除以 365 约 274 万年再乘以 12 约 3300 万个月这么算循环次数还是很大的。不过这种极端数据通常不会出现在机试里题目一般会限定年份范围在 1 到 9999 以内num 也在合理范围内。就算出现这个代码也比逐日循环快 30 倍左右。如果真的遇到 num 特别大的情况还可以再优化先在 while 循环里把“一年”当作整体跳过。比如如果 d 还剩 300 天以上就先判断当前年是否是闰年然后一次性减去 365 或 366 天让年份直接加一。这种优化能把循环次数从“月数”降到“年数”但代码复杂度会上升。我的建议是基础版本按月进位已经足够应对绝大多数题目优化版本可以作为进阶练习去写但考场上别冒险。5. 常见问题与调试实录5.1 高频易错点速查表我整理了一套自查清单每次写完日期题都对着过一遍基本能避免大多数低级错误易错点错误写法示例正确做法闰年判断不完整y % 4 0同时排除整百年再加 400 年修正二月天数写死返回28不判断闰年先查表2 月再单独判断月份天数下标混乱数组从 0 开始m - 1取数数组长度 13下标直接用月份忘记月份溢出判断只有m没有if (m 12)月份变 13 时重置为 1年份加 1循环内使用固定天数d - monthDays[2]每次都重新调用daysInMonth(y, m)输出补零格式错误用%2d导致空格填充用%02d左补零输入多组数据的循环写错只处理一次就结束while (n--)或for (int i 0; i n; i)这里我想单独展开讲一下“循环内使用固定天数”这个坑。假如你最开始读到的是 2024 年 1 月于是记下daysInMonth(2024, 1)是 31。接着 while 循环开始执行d 减去 31 后进入 2 月这时如果还拿旧的 31 去减就会把 2 月也当成 31 天处理后面所有月份全错。正确做法是每轮循环都要根据最新的 y 和 m 重新查天数。代码多写一次函数调用换来的是正确性这笔交易很划算。5.2 机试现场的调试心得与建议最后分享一些我的个人经验。第一次写这个题的时候我也想过“要不要把日期转成绝对天数再做”后来发现 KY257 这种题目不需要那么复杂反而是在小题里练熟“月份天数表 进位判断”这套模板后面做大日期差值题时才能更快上手。建议你把这个模板背下来一个 isLeap 函数一个 daysInMonth 函数外加一个 while 进位循环。这三块组合可以解决 90% 的日期类机试题。调试时最容易忽略的是“清零意识”。多组测试数据之间变量会重复使用如果你在循环里定义局部变量就不会有残留。但如果你把所有变量都定义在 main 外面第二次循环就得小心重置。我曾经因为一个全局变量m没有重置导致第二组测试用例直接从上一组的月份开始进位结果差了整整一年。从那以后我习惯把日期处理的独立逻辑都放进函数里局部变量用完即销毁从根源上杜绝这种问题。还有一个技巧提交之前用 Python 的datetime模块或者手机日历做交叉验证。比如你想验证2099-03-15 5000的结果可以写一两行 Python 代码直接算from datetime import date, timedelta print(date(2099, 3, 15) timedelta(days5000))然后拿这个输出跟自己的 C 程序对比。虽然 Python 的日期范围有限但常用的测试年份完全够用。这个习惯帮我抓出过不少“自以为对实际差一天”的隐蔽 bug。我个人在实际操作中的体会是日期类题目几乎没有“一次写对”的运气靠的都是边界测试和经验积累。KY257 这道题值得做第二遍甚至第三遍每做一遍你会更清楚自己容易在哪个环节翻车。把这篇里面提到的测试用例跑完再总结一下自己的易错点比闷头连刷十道新题更有用。