以 1582 年换历点分段,把儒略日编号分别反推为儒略历日期或格里高利历日期。
OJ: luogu
题目 ID: P7075
难度:普及+/提高-
标签:模拟数学二分
创建: 2026-07-06 08:42
更新: 2026-10-07 10:45
题意
给出若干个整数儒略日编号 r。
题目规定第 0 天是:
公元前 4713 年 1 月 1 日要求输出从这一天开始经过 r 天后的日期。
日期规则有一个非常重要的分界点:
1582-10-04及以前,用儒略历;1582-10-04的下一天直接跳到1582-10-15;1582-10-15及以后,用格里高利历;- 没有公元
0年,公元前1年的下一年是公元1年。
思路
最直接的暴力是从起点开始一天一天往后模拟。
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 22:23
* update_at: 2026-10-01 22:23
*/
// brute.cpp:小数据暴力解,逐日模拟从起点向后走 r 天,用来验证换历和公元前输出规则。
// 只适合 r 不太大的对拍场景(r 数万以内),r 大时会超时。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 日期结构体,year 用天文年份编号(公元前 4713 年 = -4712)。
struct Date {
ll year;
int month;
int day;
};
int month_days_common[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // 每月天数(二月按平年)
// 儒略历闰年:年份能被 4 整除就是闰年。
bool is_julian_leap(ll year) {
return year % 4 == 0;
}
// 格里高利历闰年:400 的倍数,或 4 的倍数但不是 100 的倍数。
bool is_gregorian_leap(ll year) {
return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
}
// 判断日期 x 是否已进入格里高利历(1582-10-15 及以后)。
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;
}
// 返回日期 x 所在月份的天数,自动根据日期选择儒略历或格里高利历闰年规则。
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;
}
// 将日期 x 推进一天,特殊处理 1582-10-04 之后直接跳到 10-15。
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++;
}
}
}
// 输出日期:year <= 0 时输出「BC」格式,否则输出公元后格式。
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--) {
ll r;
cin >> r;
// 从起点 -4712-01-01 开始,逐日走 r 步。
Date cur;
cur.year = -4712;
cur.month = 1;
cur.day = 1;
for (ll 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不存在,不能被输出。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-01 22:23
* update_at: 2026-10-01 22:23
*/
// main.cpp:把儒略日编号按换历点分段,分别用儒略历/格里高利历反推日期。
// 核心思路:先算出 1582-10-04(旧历最后一天)的编号,再分两段二分找年份、扫月份。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll START_YEAR = -4712; // 天文年份编号:公元前 4713 年记为 -4712,公元前 1 年记为 0
int month_days_common[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // 每月天数(二月按平年)
// 向下取整除法:C++ 对负数除法是截断,这里保证数学意义上的 floor。
ll floor_div(ll a, ll b) {
if (a >= 0) {
return a / b;
}
return -((-a + b - 1) / b);
}
// 统计 [left, right] 中能被 4 整除的整数个数(含端点),用 floor_div 处理负数。
ll count_multiple_of_4(ll left, ll right) {
if (left > right) {
return 0;
}
return floor_div(right, 4) - floor_div(left - 1, 4);
}
// 儒略历闰年:年份能被 4 整除就是闰年(含公元前年份)。
bool is_julian_leap(ll year) {
return year % 4 == 0;
}
// 格里高利历闰年:400 的倍数,或 4 的倍数但不是 100 的倍数。
bool is_gregorian_leap(ll year) {
return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
}
// 返回某年某月的天数;gregorian 决定用哪套闰年规则。
int month_days(ll 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;
}
// 返回某年某月某日是该年的第几天(从 1 开始)。
int day_of_year(ll 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 的天数(0-based)。
ll julian_serial(ll year, int month, int day) {
ll years = year - START_YEAR;
ll 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 的天数(0-based),只对正数年份使用。
ll gregorian_ordinal(ll year, int month, int day) {
ll y = year - 1;
ll result = y * 365 + y / 4 - y / 100 + y / 400;
result += day_of_year(year, month, day, true) - 1;
return result;
}
// 输出日期:year <= 0 时输出「BC」格式,否则输出公元后格式。
void output_date(ll year, ll month, ll day) {
if (year <= 0) {
cout << day << ' ' << month << ' ' << 1 - year << " BC\n";
} else {
cout << day << ' ' << month << ' ' << year << '\n';
}
}
// 对单个儒略日 r,先判断落在旧历还是新历,再二分年份、扫月份输出。
void solve_one(ll r) {
ll last_julian_day = julian_serial(1582, 10, 4); // 旧历最后一天
ll first_gregorian_day = last_julian_day + 1; // 新历第一天的编号
ll first_gregorian_ordinal = gregorian_ordinal(1582, 10, 15);
if (r <= last_julian_day) {
// 旧历段:二分找年份,再从 1 月扫到 12 月定位月份和日。
ll left = START_YEAR;
ll right = 1582;
while (left < right) {
ll mid = (left + right + 1) / 2;
if (julian_serial(mid, 1, 1) <= r) {
left = mid;
} else {
right = mid - 1;
}
}
ll year = left;
ll 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, remain + 1);
return;
}
remain -= days;
}
} else {
// 新历段:把编号换算成格里高利历连续天数,再二分找年份。
ll target = first_gregorian_ordinal + (r - first_gregorian_day);
ll left = 1582;
ll right = 2000000000LL;
while (left < right) {
ll mid = (left + right + 1) / 2;
if (gregorian_ordinal(mid, 1, 1) <= target) {
left = mid;
} else {
right = mid - 1;
}
}
ll year = left;
ll 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, remain + 1);
return;
}
remain -= days;
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
while (q--) {
ll r;
cin >> r;
solve_one(r);
}
return 0;
}复杂度
设询问数为 Q。
- 每次询问二分年份,时间复杂度
,其中 Y是答案年份范围; - 扫月份是常数
12; - 总时间复杂度:
; - 空间复杂度:
。
总结
这题不是普通日期模拟,真正的坑有三个:
- 公元前没有
0年,内部最好改成连续的天文年份; 1582-10-04后直接跳到1582-10-15;- 换历前后闰年规则不同。
把时间轴按换历点分成两段后,问题就变成:
天数编号 -> 二分年份 -> 扫月份 -> 输出日期本文的 main.cpp 已用逐日模拟版 brute.cpp 进行 300 组随机小范围儒略日对拍。