中 · 历法
日期相差几天
直接在两个日期之间量天数,是进退位的迷宫。换个思路:给每个日期编一个流水号 —— 距某个固定纪元过了多少天 —— 差值就变成一次减法。历法的所有丑陋都藏进换算里,而这正是它该在的地方。
一句话答案写一个 <code>serial(y, m, d)</code> = 整年数 + m 之前整月的天数 + d(月长按闰年调整)。答案 = <code>serial(y2,m2,d2) - serial(y1,m1,d1)</code>。
题目
输入: 两行:y1 m1 d1 和 y2 m2 d2(1900 ≤ y ≤ 2100),第一行日期不晚于第二行。
输出: 从第一行日期到第二行日期的天数(同一天 → 0)。
样例:
| 输入 | 输出 |
|---|---|
2000 1 1 | 365 |
1900 2 28 | 1 |
第二个样例就是一行写尽的世纪陷阱:1900 能被 100 整除但不能被 400 整除,所以不是闰年 —— 二月只有 28 天,间隔是 1。同样的日期放在 2000 年,间隔就是 2。
你的解答
「运行我的输入」用的自定义输入
判题在浏览器本地运行。死循环会在 3 秒后被自动中断 —— 但还是确保每个循环都有出口。代码随打随存。
卡住了?提示
提示 1 — 流水号里的三层
serial(y, m, d) 加三块:y 之前的整年(每年 365 或 366 —— 从纪元年起循环)、y 年 m 之前的整月(循环调用 monthDays)、以及 d 本身。各是一小段循环,都不需要机灵。
提示 2 — 闰年规则是三个条件
被 4 整除 → 闰,但被 100 整除 → 不闰,但被 400 整除 → 闰。一行编码全部三条:(y % 4 == 0 && y % 100 != 0) || y % 400 == 0。先在 1900(否)、2000(是)、2100(否)、2004(是)上测一遍再谈相信。
提示 3 — 差值,而不是两次擦边
新手常试着从 date1 逐月走到 date2,手动处理月末 —— 那条路上有五个差一错误的入口。流水号设计把入口全部拆掉:两边各换算一次,相减,完事 —— 而且同一组流水号以后还能免费回答「这天是星期几」。
题解(先自己试试!)
#include <iostream>
using namespace std;
bool isLeap(int y) {
return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
}
int monthDays(int y, int m) {
int d[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (m == 2 && isLeap(y)) return 29;
return d[m];
}
long long serial(int y, int m, int d) {
long long s = 0;
for (int yy = 1900; yy < y; yy++) s += isLeap(yy) ? 366 : 365;
for (int mm = 1; mm < m; mm++) s += monthDays(y, mm);
return s + d;
}
int main() {
int y1, m1, d1, y2, m2, d2;
cin >> y1 >> m1 >> d1 >> y2 >> m2 >> d2;
cout << serial(y2, m2, d2) - serial(y1, m1, d1) << endl;
return 0;
}
流水号技巧是计算界最老的思想之一 —— Unix 时间戳就是它,只是换成了 1970 年起数的秒。你在这里搭的一切(纪元、月表、闰年规则)都能迁移:加一个星期计算(serial % 7)、加时区,就是一个日期库的骨架。规则本身是 闰年判断 里的老朋友 —— 这道题是它们开始挣钱的地方。
深度解析
为什么这样做对
给每个日期编流水号 —— 距固定纪元的天数 —— 差值就成了减法。流水号由三层无聊的构造组成(整年、整月、当月第几天),各是一小段循环;历法的全部丑陋都住在换算里,可以被一次性测透,而不是抹得到处都是。
代价
循环版每次换算 O(年数) —— 1900–2100 范围是 200 次迭代 —— 最终减法 O(1)。存在常数时间的公式版(带闰年修正),但循环版可证正确且足够快。
常见错法
- 世纪规则:1900 和 2100 都<strong>不是</strong>闰年(被 100 整除但不被 400 整除),2000 <strong>是</strong>。只用 2000 测试,这个 bug 会永远藏身。
- monthDays 里的二月长度必须查当前年份的 isLeap —— 只按月份键控的固定 28/29 表会在闰年翻车。
- 包含式计数:同一天到自己是 0,平年 2 月 28 到 3 月 1 是 1。这里的差一会把整个答案翻倍或减半。
下一步
闰年规则来自 闰年判断;流水号技巧是缩微版 Unix 时间戳 —— 加上 serial % 7 就免费得到星期几。
记住这两句
- 流水号把日期算术变成减法 —— 换算、相减、完事。
- 闰年一行版:(y%4==0 && y%100!=0) || y%400==0 —— 在 1900、2000、2100 上各测一遍。
🐞 这段是错的 —— 点开,亲手让它挂一次
闰年只测 y % 4 == 0 —— 1900 和 2100 被当成了闰年
#include <iostream>
using namespace std;
bool isLeap(int y) {
return y % 4 == 0; // bug: century rule missing
}
int monthDays(int y, int m) {
int d[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (m == 2 && isLeap(y)) return 29;
return d[m];
}
long long serial(int y, int m, int d) {
long long s = 0;
for (int yy = 1900; yy < y; yy++) s += isLeap(yy) ? 366 : 365;
for (int mm = 1; mm < m; mm++) s += monthDays(y, mm);
return s + d;
}
int main() {
int y1, m1, d1, y2, m2, d2;
cin >> y1 >> m1 >> d1 >> y2 >> m2 >> d2;
cout << serial(y2, m2, d2) - serial(y1, m1, d1) << endl;
return 0;
}