以 1582 年换历点分段,把儒略日编号分别反推为儒略历日期或格里高利历日期。
OJ: luogu
题目 ID: P7075
难度:普及+/提高
标签:模拟数学二分
日期: 2026-07-06 08:42
题意
给出若干个整数儒略日编号 r。
题目规定第 0 天是:
公元前 4713 年 1 月 1 日要求输出从这一天开始经过 r 天后的日期。
日期规则有一个非常重要的分界点:
1582-10-04及以前,用儒略历;1582-10-04的下一天直接跳到1582-10-15;1582-10-15及以后,用格里高利历;- 没有公元
0年,公元前1年的下一年是公元1年。
思路
最直接的暴力是从起点开始一天一天往后模拟。
// brute.cpp:小数据模拟每天向后走,用来验证换历和公元前输出规则。
#include <bits/stdc++.h>
using namespace std;
struct Date {
long long year;
int month;
int day;
};
int month_days_common[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
bool is_julian_leap(long long year) {
return year % 4 == 0;
}
bool is_gregorian_leap(long long year) {
return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
}
bool is_gregorian_date(const Date &x) {
if (x.year > 1582) {
return true;
}
if (x.year < 1582) {
return false;
}
if (x.month > 10) {
return true;
}
if (x.month < 10) {
return false;
}
return x.day >= 15;
}
int month_days(const Date &x) {
if (x.month != 2) {
return month_days_common[x.month];
}
if (is_gregorian_date(x)) {
return is_gregorian_leap(x.year) ? 29 : 28;
}
return is_julian_leap(x.year) ? 29 : 28;
}
void next_day(Date &x) {
if (x.year == 1582 && x.month == 10 && x.day == 4) {
x.day = 15;
return;
}
x.day++;
if (x.day > month_days(x)) {
x.day = 1;
x.month++;
if (x.month > 12) {
x.month = 1;
x.year++;
}
}
}
void output_date(const Date &x) {
if (x.year <= 0) {
cout << x.day << ' ' << x.month << ' ' << 1 - x.year << " BC\n";
} else {
cout << x.day << ' ' << x.month << ' ' << x.year << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
while (q--) {
long long r;
cin >> r;
Date cur;
cur.year = -4712;
cur.month = 1;
cur.day = 1;
for (long long i = 0; i < r; i++) {
next_day(cur);
}
output_date(cur);
}
return 0;
}这个暴力非常适合检查边界:
- 公元前年份怎么输出;
1582-10-04后面是否跳到1582-10-15;- 儒略历和格里高利历的闰年规则是否不同。
但 r 可以非常大,询问也很多,不能逐日模拟。
使用天文年份编号
题目说没有公元 0 年,这对计算很麻烦。
我们内部使用“天文年份编号”:
| 普通写法 | 内部年份 |
|---|---|
| 公元前 4713 年 | -4712 |
| 公元前 1 年 | 0 |
| 公元 1 年 | 1 |
输出时再转回来:
如果 year <= 0,那么输出 1 - year,并加 BC这样做的好处是年份可以连续加减。
换历点
先计算:
last_julian_day = 1582-10-04 的儒略日编号
first_gregorian_day = last_julian_day + 1如果 r <= last_julian_day,答案在旧历部分,按儒略历反推。
否则答案在新历部分,先把 r 转成格里高利历里的连续天数,再反推日期。
儒略历部分
起点是天文年份 -4712 的 1 月 1 日。
儒略历闰年规则非常简单:
年份能被 4 整除就是闰年注意这在天文年份编号下也适用于公元前年份。比如公元前 1 年是内部年份 0,确实是 4 的倍数。
要计算某年某月某日的编号:
完整年份天数 + 当年已经过去的天数完整年份天数是:
(year - START_YEAR) * 365 + [START_YEAR, year-1] 中 4 的倍数年份个数因为年份可能为负数,统计 4 的倍数时要使用数学意义上的向下取整除法,不能直接依赖 C++ 对负数除法的截断结果。
格里高利历部分
格里高利历只会用于 1582-10-15 及之后,年份都是正数。
对于公元 1 年以来的格里高利历,某年之前的完整天数为:
(year - 1) * 365
+ (year - 1) / 4
- (year - 1) / 100
+ (year - 1) / 400这就是“4 年一闰,100 年不闰,400 年又闰”的公式。
我们知道 1582-10-15 对应换历后的第一天,因此:
target = ordinal(1582-10-15) + (r - first_gregorian_day)然后在格里高利历中找 target 对应的年月日。
如何从天数反推年月日
无论是儒略历还是格里高利历,都可以:
- 二分找到最大的
year,满足这一年的1 月 1 日不晚于目标天数; - 用剩余天数从
1月扫到12月; - 得到日。
月份只有 12 个,所以直接扫月即可。
关键边界
这几个日期最容易错:
| 编号含义 | 日期 |
|---|---|
r = 0 |
1 1 4713 BC |
1582-10-04 |
旧历最后一天 |
1582-10-15 |
新历第一天 |
中间的:
1582-10-05 到 1582-10-14不存在,不能被输出。
代码
// main.cpp:把儒略日编号按换历点分段,分别用儒略历/格里高利历反推日期。
#include <bits/stdc++.h>
using namespace std;
const long long START_YEAR = -4712; // 天文年份:公元前 4713 年记为 -4712。
int month_days_common[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
long long floor_div(long long a, long long b) {
if (a >= 0) {
return a / b;
}
return -((-a + b - 1) / b);
}
long long count_multiple_of_4(long long left, long long right) {
if (left > right) {
return 0;
}
return floor_div(right, 4) - floor_div(left - 1, 4);
}
bool is_julian_leap(long long year) {
return year % 4 == 0;
}
bool is_gregorian_leap(long long year) {
return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
}
int month_days(long long year, int month, bool gregorian) {
if (month != 2) {
return month_days_common[month];
}
if (gregorian) {
return is_gregorian_leap(year) ? 29 : 28;
}
return is_julian_leap(year) ? 29 : 28;
}
int day_of_year(long long year, int month, int day, bool gregorian) {
int result = day;
for (int m = 1; m < month; m++) {
result += month_days(year, m, gregorian);
}
return result;
}
// 1582-10-04 及以前使用:从起点 -4712-01-01 到儒略历 date 的天数。
long long julian_serial(long long year, int month, int day) {
long long years = year - START_YEAR;
long long leaps = count_multiple_of_4(START_YEAR, year - 1);
return years * 365 + leaps + day_of_year(year, month, day, false) - 1;
}
// 格里高利历中,从 1-01-01 到 date 的天数。
long long gregorian_ordinal(long long year, int month, int day) {
long long y = year - 1;
long long result = y * 365 + y / 4 - y / 100 + y / 400;
result += day_of_year(year, month, day, true) - 1;
return result;
}
void output_date(long long year, int month, int day) {
if (year <= 0) {
cout << day << ' ' << month << ' ' << 1 - year << " BC\n";
} else {
cout << day << ' ' << month << ' ' << year << '\n';
}
}
void solve_one(long long r) {
long long last_julian_day = julian_serial(1582, 10, 4);
long long first_gregorian_day = last_julian_day + 1;
long long first_gregorian_ordinal = gregorian_ordinal(1582, 10, 15);
if (r <= last_julian_day) {
long long left = START_YEAR;
long long right = 1582;
while (left < right) {
long long mid = (left + right + 1) / 2;
if (julian_serial(mid, 1, 1) <= r) {
left = mid;
} else {
right = mid - 1;
}
}
long long year = left;
long long remain = r - julian_serial(year, 1, 1);
for (int month = 1; month <= 12; month++) {
int days = month_days(year, month, false);
if (remain < days) {
output_date(year, month, (int)remain + 1);
return;
}
remain -= days;
}
} else {
long long target = first_gregorian_ordinal + (r - first_gregorian_day);
long long left = 1582;
long long right = 2000000000LL;
while (left < right) {
long long mid = (left + right + 1) / 2;
if (gregorian_ordinal(mid, 1, 1) <= target) {
left = mid;
} else {
right = mid - 1;
}
}
long long year = left;
long long remain = target - gregorian_ordinal(year, 1, 1);
for (int month = 1; month <= 12; month++) {
int days = month_days(year, month, true);
if (remain < days) {
output_date(year, month, (int)remain + 1);
return;
}
remain -= days;
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
while (q--) {
long long r;
cin >> r;
solve_one(r);
}
return 0;
}复杂度
设询问数为 Q。
- 每次询问二分年份,时间复杂度
,其中 Y是答案年份范围; - 扫月份是常数
12; - 总时间复杂度:
; - 空间复杂度:
。
总结
这题不是普通日期模拟,真正的坑有三个:
- 公元前没有
0年,内部最好改成连续的天文年份; 1582-10-04后直接跳到1582-10-15;- 换历前后闰年规则不同。
把时间轴按换历点分成两段后,问题就变成:
天数编号 -> 二分年份 -> 扫月份 -> 输出日期本文的 main.cpp 已用逐日模拟版 brute.cpp 进行 300 组随机小范围儒略日对拍。