
我最早刷到这道“基础题-12-求第几天”的时候心里想的是这不就是输入年月日输出第几天吗有什么好写的。结果在提交区看到好几个同学在问“为什么我的答案总是少一天”“2月怎么处理才对”我才意识到这道题其实是一块很标准的试金石专治各种粗心和基础不牢。它的考点不算刁钻但覆盖了日期类题目的三个核心基本功闰年判断、月份天数映射、边界情况处理。如果你能一次写对说明这几个点你是真会了如果写不对那今天这篇就当陪你把这个坑一步一步踩平了。1. 题目本身极简真正的坑藏在闰年和边界里1.1 先把题意翻译成程序逻辑题目描述一句话就能讲完给定一个年月日计算这一天是这一年的第几天。比如输入 2023 年 3 月 1 日因为 1 月 31 天2 月 28 天所以答案是 31 28 1 60输出 60 就对了。程序要做的事其实只有三步读入三个整数 y、m、d把从 1 月到 m-1 月的天数全部加起来最后再加上本月的天数 d输出总和。这逻辑听起来人畜无害实际一写就出事。最典型的就是把 2 月当成永远 28 天或者干脆把当前月份的天数也加进去了。比如有人写了一个循环从 1 月加到 m 月再把 d 加上输入 2023 年 1 月 1 日的时候他算出 31 1 32直接多了 31 天。这种错误你自己盯自己的代码往往盯不出来因为思路一旦跑偏眼睛会跟着思路走。所以第一步不是写代码而是把“累加 months[1] 到 months[m-1]最后加 d”这个公式写在草稿纸上确认自己没有把当月算进去、没有把 d 重复累加。别小看这一步它能帮你省掉好几次无谓的 Wrong Answer。1.2 两个隐藏考点月份天数和“第几天”的定义另一个容易阴沟翻船的点是月份天数表记不牢。很多人脑子里只有“一三五七八十腊三十一天永不差”但一落到代码里就含糊了。尤其 4 月、6 月、9 月、11 月是 30 天2 月最特殊平年 28 天、闰年 29 天。如果这几个数字记错题目输出差的就不是一天两天了。再一个概念上的小坑这道题问的是“第几天”所以 1 月 1 日答案是 1而不是 0。有些同学可能是受编程里“下标从 0 开始”的习惯影响总觉得第一天应该算 0结果整个答案全部少 1。这个和数组下标无关它就是一个朴素的日历计数问题你只需要老老实实把已经过去的天数加 1 就行。把这两点想清楚等于地基打好了后面不管用什么写法都不容易跑偏。2. 闰年判断一个条件定生死背错公式就全错2.1 为什么“四年一闰”是不完整的闰年判断是这道题的核心也是大家错得最集中的地方。最经典的理解是“能被 4 整除就是闰年”这句话在日常生活中够用但在题目里不够严谨。因为公历的闰年规则是能被 4 整除但不能被 100 整除或者能被 400 整除。用 C 写出来就是bool isLeap(int year) { return (year % 4 0 year % 100 ! 0) || (year % 400 0); }这个公式我建议直接背下来不用每次现推。重点是理解它为什么要多一个“不能被 100 整除”的限定因为 1900 年能被 4 整除但它是平年2 月只有 28 天。如果不加这个限定1900 年 3 月 1 日就会被你算成第 61 天而正确答案是 60 天。很多 OJ 的测试数据就是喜欢挑这种整百年份来卡人因为初学者最容易在“四年一闰”的直觉上翻车。2.2 闰年只影响 2 月别让其他月份跟着遭殃闰年带来的唯一变化是 2 月从 28 天变成 29 天其他月份完全不受影响。所以正确的处理方式是先按平年的月份天数表累加如果判断是闰年并且月份大于 2最后再加 1 天。注意这里有个极易写错的地方很多人判断出闰年后不管三七二十一直接 sum结果 1 月和 2 月也被多加了一天。输入 2024 年 1 月 15 日的时候正确答案是 15他却输出 16。这类错误我自己在帮别人看代码时碰到过好几次几乎都是同一个原因闰年加一天的条件写成了 if (isLeap(y))而不是 if (isLeap(y) m 2)。用大白话总结闰年的额外 1 天只有在 2 月过完之后才真正体现出来。你求的是 2 月 29 日当天的第几天那 31 29 60加不加这 1 天都无所谓因为 2 月 29 日本身就是答案的一部分但你求 1 月 15 日时2 月的 29 天还没发生凭什么提前加2.3 一个稳健的代码习惯把闰年判断封装成函数很多初学者喜欢把闰年判断直接写在 main 函数里比如写一个 if-else 判断年份然后再在另一个 if 里判断月份二层嵌套下来代码已经乱成一团。我建议把闰年判断单独拎出来写成上面的 isLeap 函数。原因很简单逻辑单一测起来方便后面做更复杂的日期题时比如日期差、星期几这个函数可以直接复用代码阅读起来一眼能看出意图。这不算过度设计只是一种很好的习惯。刷题不只是为了 AC更是为了把基础代码块打磨成可以随时调用的模板。我后面还会专门说模板的事这里先卖个关子。3. 三种实现方案从“能过”到“好看”逐个拆解3.1 方案一if 暴力分支直白但最容易写漏新手最爱用的写法是拿 if-else 把 12 个月全部列出来if (m 1) { sum d; } else if (m 2) { sum 31 d; } else if (m 3) { sum 31 28 d; } else if (m 4) { sum 31 28 31 d; } // 后面继续写到 m 12这种写法能过但问题很多。首先代码极其冗长12 个分支写下来光看就累。其次非常容易漏改闰年分支——m 3 的时候普通人脑中会直接写 31 28 d根本想不起来要不要判断闰年。最后这种写法几乎没有任何扩展性换一道题、换个输入格式整段代码就要推翻重写。我的评价是如果哪天真在考场或者比赛里慌了用它保底没问题但平时练习我不建议把它当成第一方案。3.2 方案二switch 倒序累加思路清奇但容易被忽略还有一种挺有意思的写法利用 switch 的“贯穿”特性从 m-1 月份一路加到 1 月int sum d; switch (m - 1) { case 11: sum 30; case 10: sum 31; case 9: sum 30; case 8: sum 31; case 7: sum 31; case 6: sum 30; case 5: sum 31; case 4: sum 30; case 3: sum 31; case 2: sum 28; case 1: sum 31; }这个写法的核心是利用了 switch 的 fall-through 特性当 m 等于 4 时m - 1 等于 3程序会从 case 3 开始一直往下执行到 case 1把 3 月、2 月、1 月的天数全部加上。所以最后 sum 就是 1 到 m-1 月所有天数和 d。它的优点是代码相对紧凑思路也挺巧妙缺点是 fall-through 这个语法特性对初学者不友好很容易让人看懵而且同样要处理“闰年则多 1 天”的分支。如果你对 switch 的贯穿机制很熟这个方案可以作为备选如果不熟就别在正式提交时冒险了。3.3 方案三前缀和数组我最推荐的写法真正的推荐方案是开一个月份天数表然后累加前 m-1 个月#include iostream using namespace std; bool isLeap(int year) { return (year % 4 0 year % 100 ! 0) || (year % 400 0); } int main() { int y, m, d; int daysInMonth[] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; while (cin y m d) { int sum 0; for (int i 1; i m; i) { sum daysInMonth[i]; } if (isLeap(y) m 2) { sum 1; } sum d; cout sum endl; } return 0; }我来说说这段代码里几个很细节、但非常关键的设计。第一为什么 daysInMonth 数组第一个元素是 0因为我想让下标和月份直接对应daysInMonth[1] 表示 1 月的天数daysInMonth[2] 表示 2 月的天数。这样写循环时i m就不会出现下标混乱。如果你定义成int daysInMonth[12] {31, 28, ...}那 1 月对应下标 0循环里就得写成i m - 1不仅拗口还特别容易出错。一个 0 占位符换来的是整个逻辑的清晰非常划算。第二为什么累加完成后才判断闰年因为这样顺序最自然先把平年的月份天数全部算完如果发现是闰年而且月份确实已经过了 2 月再补上额外的一天。如果你把闰年判断放在循环之前会凭空多出很多分支考虑比如“当前月份是不是 2 月”“累加过程中会不会经过 2 月”复杂度一下子上升了。第三while (cin y m d) 这个循环结构非常重要。很多 OJ 的题目描述会写“输入多组测试数据”但也有少数题目没写清楚实际测试时却连续给多行输入。用 while 读入是最稳妥的写法它会在文件结束符或输入结束时自动退出循环。如果你只写一次 cin那么多组数据的情况下你的程序只会处理第一组后面全部漏掉。到这里三种写法放在一起对比你可以很清楚地看到数组写法把复杂的分支转换成了一张简单的“查表”动作这正是数据表驱动思想在基础题里的第一次亮相。以后遇到更复杂的日期题目你会发现这个思想会反复出现。4. 提交之前先用这组边界数据自测一遍4.1 最容易藏雷的输入2 月 29 日和整百年份程序写完之后建议不要急着点提交先手动测几个边界数据。日期题有个特点普通数据看不出问题但边界数据一上来错误就藏不住了。我最常用的自测表如下输入期望输出测试意图2023 1 11平年第一天2023 12 31365平年最后一天2024 1 1515闰年 1 月不能多加 1 天2024 2 2960闰年 2 月 29 日2024 12 31366闰年最后一天1900 3 160能被 4 整除但不是闰年2000 3 161能被 400 整除的闰年这几组数据覆盖了最常见的出错点平年跨年、闰年 2 月、整百年份、闰年 1 月不应加天数。如果你的程序都能通过那逻辑上基本就稳了。特别提醒 1900 和 2000 这两组很多人就是栽在这里只写了year % 4 0导致 1900 被错判为闰年3 月 1 日输出 61。4.2 多组输入的本质循环结构决定你能得多少分前面提到了 while (cin ...)这里再展开讲一下。C 里cin y m d这个表达式在成功读入数据时会返回 cin 对象本身也就是真正的 bool 值当读入失败或遇到文件结束时会返回一个可转换为 false 的值。所以while (cin y m d)的意思是一直读读到读不进去为止。在本地测试多组输入时Windows 下按 CtrlZ 加回车Linux 或 macOS 下按 CtrlD表示输入结束。很多初学者不知道这一点在终端里看到光标一直闪烁以为程序卡死了其实是在等你结束输入。平台评测时测试文件会自动在末尾提供结束标志所以代码里不需要再额外处理。如果你确定题目就是单组输入那去掉 while 直接写也不是不行但问题是很多题目描述里不会写得很明确。为了稳妥我建议不管三七二十一把 while 写上反正单组输入时它也只循环一次没有任何副作用。4.3 别人踩过最典型的 Wrong Answer 现场我见过一个同学的代码逻辑写得还算顺畅但结果总是比正确答案多 1。查了半天发现他把 sum 初始化成了 1然后循环里又从 1 月加到 m-1 月最后又加 d。等于是把“这一天”连续加了两遍。这种问题肉眼最难发现因为整个运算过程从头到尾都很“顺”你不会想到问题出在初始化上。另一个典型错误是把 daysInMonth 数组长度为 12然后循环里for (int i 1; i m; i)。当 m 12 时i 会取到 12但数组下标只有 0 到 11直接越界。虽然 C 的数组越界不一定会立刻崩溃但它会读到一块未知内存结果自然变得不可预测。正确做法要么把数组长度定成 13要么循环条件写i m二者选其一就不会出问题。你可以把这些错误归成三类边界多 1 少 1、闰年判断条件写错、数组索引越界。这三类基本覆盖了这道题的大部分 Wrong Answer 原因。提交前针对这三类逐一排查效率远高于反复盲交。5. 做完这道题你该顺手建一个日期类模板库了5.1 反向题目已知第几天怎么求出日期“求第几天”的反向题目也很常见给你年份 y 和天数 n求对应的日期。比如 2024 年的第 60 天是几月几日答案是 2 月 29 日。实现思路刚好反过来先把闰年的 2 月天数改成 29然后从 1 月开始逐个减去每个月的天数直到剩余天数不够减为止。int y, n; cin y n; int daysInMonth[] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (isLeap(y)) { daysInMonth[2] 29; } int month 1; while (n daysInMonth[month]) { n - daysInMonth[month]; month; } cout y - month - n endl;这段代码里有一个非常容易忽略的细节如果你直接修改了 daysInMonth[2] 29那么这个数组就变成“闰年专用表”了。同一个程序里如果再想用它计算平年日期就必须先把 2 月改回去或者重新声明一个临时数组。否则平年会被错误地当成闰年处理。这个坑我在写日期差题目时踩过一次后来养成了习惯凡是会修改天数表的函数都要明确说明它对数组的影响或者干脆复制一份再改。5.2 把 isLeap 和 dayOfYear 存成自己的模板函数刷题刷到后面你会发现日期类题目其实就那么几种套路函数名来回就是 isLeap、dayOfYear、daysBetween 这几个。与其每次都从头写不如在做完这道基础题之后顺手把自己的“日期工具箱”建起来。我这里说一个最基础但最常用的两个函数bool isLeap(int year) { return (year % 4 0 year % 100 ! 0) || (year % 400 0); } int dayOfYear(int y, int m, int d) { int daysInMonth[] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int sum 0; for (int i 1; i m; i) { sum daysInMonth[i]; } if (isLeap(y) m 2) { sum 1; } return sum d; }每次遇到“求某日是第几天”直接调用 dayOfYear遇到“日期差”问题把两个日期各自转换成相对某基准日的总天数再相减遇到“星期几”问题则可以从一个已知的某年某月某日是星期几出发用天数差对 7 取模。这些高级题目的地基都是今天这道题里打下的。5.3 一道基础题带来的算法思维启蒙我觉得这道题真正让人受益的地方不是“我会写求第几天了”而是它让你第一次体会到怎么把一类问题抽象成可复用的代码块。isLeap 是一个判断条件daysInMonth 是一张数据表dayOfYear 是一个操作函数。这三个东西放在一起几乎可以解决所有相对简单的日期问题。以后你再做“求日期差”的时候你会发现最大的难题被拆成了三步先算两个日期分别离公元 1 年 1 月 1 日多少天然后一减就得到相差天数。这里的核心函数就是你今天写的 isLeap、dayOfYear。我们不需要去背什么复杂的数学公式只需要把基础模块搭稳上层问题自然一层层解开。我当时做完这道题之后顺手把这两个函数存进了自己的代码模板文件后面刷了十几道日期相关的题目基本都能在几分钟内套出主逻辑。这就是基础题应有的价值你提前把路修好了后面跑起来自然快。最后再分享一个我平时调试日期题的小习惯不要只测题目给的样例而是先自己脑内把四组数据过一遍——平年 2 月末、闰年 2 月末、平年 12 月 31 日、闰年 12 月 31 日。这四组只要对了这道题基本跑不偏。如果哪天你在日期题上反复 Wrong Answer也不用慌回到“闰年是否多加了、当前月是否误加了、数组有没有越界”这三个老地方去查八成能定位问题。