把答案拆成完整年份、完整月份和当月已过天数,再对 1582 年换历与缺失 10 天分段处理。
OJ: luogu
题目 ID: P8831
难度:普及-
标签:模拟数学思维
日期: 2026-06-19 02:47
题意
给一个日期,要求计算它距离 1JAN1 已经过了多少天。
这里的“经过了多少天”指的是:从 1JAN1 走到这个日期之前,一共经历了多少个真实存在的日期,所以 1JAN1 的答案是 0。
题目有两个特殊规则:
1582年之前按儒略历算闰年:4的倍数就是闰年;- 从
1582年开始改成新的规则,并且1582-10-04的下一天直接是1582-10-15。
这个跳转最容易出错,可以先记住:
| 当前日期 | 下一天 |
|---|---|
4OCT1582 |
15OCT1582 |
也就是说 1582-10-05 到 1582-10-14 这 10 天根本不存在,统计天数时不能把它们算进去。
思路
最直接的做法是从 1JAN1 开始一天一天往后推。
普通情况就走到下一天;但如果当前是 1582-10-04,下一天要直接跳到 1582-10-15。
这个版本最贴近题意,也最容易帮助读者理解题目的特殊规则:
#include <bits/stdc++.h>
using namespace std;
const string MONTH_NAME[13] = {
"", "JAN", "FEB", "MAR", "APR", "MAY", "JUN",
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"
};
const int MONTH_DAY[2][13] = {
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
struct Date {
int day;
int month;
int year;
};
string s;
Date target, cur;
bool is_julian_leap(int year) {
return year % 4 == 0;
}
bool is_gregorian_leap(int year) {
if (year % 400 == 0) {
return true;
}
if (year % 100 == 0) {
return false;
}
return year % 4 == 0;
}
int get_month_id(const string &month_str) {
for (int i = 1; i <= 12; i++) {
if (MONTH_NAME[i] == month_str) {
return i;
}
}
return 0;
}
void parse_input() {
cin >> s;
int pos = 0;
while (pos < (int) s.size() && isdigit((unsigned char) s[pos])) {
pos++;
}
target.day = stoi(s.substr(0, pos));
target.month = get_month_id(s.substr(pos, 3));
target.year = stoi(s.substr(pos + 3));
}
int get_month_day(int year, int month) {
bool leap = false;
if (year < 1582) {
leap = is_julian_leap(year);
}
else if (year > 1582) {
leap = is_gregorian_leap(year);
}
return MONTH_DAY[leap][month];
}
bool same_date(const Date &a, const Date &b) {
return a.day == b.day && a.month == b.month && a.year == b.year;
}
void next_day(Date &x) {
// 这是题目里最特别的一条规则:1582-10-04 的下一天直接变成 1582-10-15。
if (x.year == 1582 && x.month == 10 && x.day == 4) {
x.day = 15;
return;
}
int limit = get_month_day(x.year, x.month);
if (x.day < limit) {
x.day++;
return;
}
x.day = 1;
if (x.month < 12) {
x.month++;
}
else {
x.month = 1;
x.year++;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
parse_input();
cur.day = 1;
cur.month = 1;
cur.year = 1;
long long ans = 0;
while (!same_date(cur, target)) {
next_day(cur);
ans++;
}
cout << ans << '\n';
return 0;
}但如果目标年份很大,这样逐天模拟显然太慢。
我们真正要算的,其实只是“目标日期前面一共有多少天”,所以可以直接分三段统计:
- 目标年份之前的完整年份;
- 当前年份中,目标月份之前的完整月份;
- 当前月份里,目标日期之前已经过去的天数。
先算完整年份
如果目标年份 y <= 1582,那么前面的完整年份都按儒略历处理,闰年个数就是:
(y - 1) / 4
所以这一部分天数是:
365 * (y - 1) + (y - 1) / 4
如果 y > 1582,就拆成三段:
1..1581:按儒略历统计;1582:这一年实际只有355天;1583..y-1:按格里高利历统计。
格里高利历中,1..n 的闰年个数可以用:
n / 4 - n / 100 + n / 400
直接算出来。
再算完整月份
这一部分和普通日期题差不多:
- 先根据当前年份判断二月是
28天还是29天; - 再把当前月之前的月份天数累加起来。
注意 1582 年本身不是闰年,所以这里不会遇到额外麻烦。
最后算当前月已过天数
这一部分就是 day - 1。
处理删掉的 10 天
如果日期在 1582 年内,并且不早于 1582-10-15,那么它前面那 10 个不存在的日期原本会被普通公式算进去,所以最后要减去 10。
如果年份已经大于 1582,这 10 天已经被算进“前面完整年份”的 355 天里了,不能重复减。
这样就得到了最终答案。
代码
#include <bits/stdc++.h>
using namespace std;
const string MONTH_NAME[13] = {
"", "JAN", "FEB", "MAR", "APR", "MAY", "JUN",
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"
};
const int MONTH_DAY[2][13] = {
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
string s;
long long day_num, year_num;
int month_num;
bool is_julian_leap(long long year) {
return year % 4 == 0;
}
bool is_gregorian_leap(long long year) {
if (year % 400 == 0) {
return true;
}
if (year % 100 == 0) {
return false;
}
return year % 4 == 0;
}
long long count_gregorian_leap(long long year) {
return year / 4 - year / 100 + year / 400;
}
int get_month_id(const string &month_str) {
for (int i = 1; i <= 12; i++) {
if (MONTH_NAME[i] == month_str) {
return i;
}
}
return 0;
}
void parse_input() {
cin >> s;
int pos = 0;
while (pos < (int) s.size() && isdigit((unsigned char) s[pos])) {
pos++;
}
day_num = stoll(s.substr(0, pos));
month_num = get_month_id(s.substr(pos, 3));
year_num = stoll(s.substr(pos + 3));
}
long long days_before_year(long long year) {
if (year <= 1) {
return 0;
}
if (year <= 1582) {
long long last_year = year - 1;
return last_year * 365 + last_year / 4;
}
// 1..1581 按儒略历计算,1582 年实际少了 10 天。
long long ans = 365LL * 1581 + 1581 / 4;
ans += 355;
long long last_year = year - 1;
ans += (last_year - 1582) * 365;
ans += count_gregorian_leap(last_year) - count_gregorian_leap(1582);
return ans;
}
long long days_before_month(long long year, int month) {
bool leap = false;
if (year < 1582) {
leap = is_julian_leap(year);
}
else if (year > 1582) {
leap = is_gregorian_leap(year);
}
long long ans = 0;
for (int i = 1; i < month; i++) {
ans += MONTH_DAY[leap][i];
}
return ans;
}
bool after_cutover(long long year, int month, long long day) {
if (year != 1582) {
return false;
}
if (month > 10) {
return true;
}
if (month < 10) {
return false;
}
return day >= 15;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
parse_input();
long long ans = 0;
ans += days_before_year(year_num);
ans += days_before_month(year_num, month_num);
ans += day_num - 1;
// 只有 1582 年内、并且落在 10 月 15 日及之后的日期,
// 才需要在当年的统计结果里额外减去不存在的 10 天。
if (after_cutover(year_num, month_num, day_num)) {
ans -= 10;
}
cout << ans << '\n';
return 0;
}复杂度
- 时间复杂度:
- 空间复杂度:
总结
这题的关键不是逐天模拟,而是把答案拆成“完整年份 + 完整月份 + 当前月已过天数”。
真正需要单独处理的只有两件事:
1582年前后闰年规则不同;1582-10-05到1582-10-14不存在,所以换历后的日期要额外减去10天。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

