[CSP-S 2020] 儒略日

GitHub跳转原题关系图返回列表

以 1582 年换历点分段,把儒略日编号分别反推为儒略历日期或格里高利历日期。

OJ: luogu

题目 ID: P7075

难度:普及+/提高

标签:模拟数学二分

日期: 2026-07-06 08:42

题意

给出若干个整数儒略日编号 r

题目规定第 0 天是:

text
公元前 4713 年 1 月 1 日

要求输出从这一天开始经过 r 天后的日期。

日期规则有一个非常重要的分界点:

  1. 1582-10-04 及以前,用儒略历;
  2. 1582-10-04 的下一天直接跳到 1582-10-15
  3. 1582-10-15 及以后,用格里高利历;
  4. 没有公元 0 年,公元前 1 年的下一年是公元 1 年。

思路

最直接的暴力是从起点开始一天一天往后模拟。

cpp
// 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

输出时再转回来:

text
如果 year <= 0,那么输出 1 - year,并加 BC

这样做的好处是年份可以连续加减。

换历点

先计算:

text
last_julian_day = 1582-10-04 的儒略日编号
first_gregorian_day = last_julian_day + 1

如果 r <= last_julian_day,答案在旧历部分,按儒略历反推。

否则答案在新历部分,先把 r 转成格里高利历里的连续天数,再反推日期。

儒略历部分

起点是天文年份 -47121 月 1 日

儒略历闰年规则非常简单:

text
年份能被 4 整除就是闰年

注意这在天文年份编号下也适用于公元前年份。比如公元前 1 年是内部年份 0,确实是 4 的倍数。

要计算某年某月某日的编号:

text
完整年份天数 + 当年已经过去的天数

完整年份天数是:

text
(year - START_YEAR) * 365 + [START_YEAR, year-1] 中 4 的倍数年份个数

因为年份可能为负数,统计 4 的倍数时要使用数学意义上的向下取整除法,不能直接依赖 C++ 对负数除法的截断结果。

格里高利历部分

格里高利历只会用于 1582-10-15 及之后,年份都是正数。

对于公元 1 年以来的格里高利历,某年之前的完整天数为:

text
(year - 1) * 365
+ (year - 1) / 4
- (year - 1) / 100
+ (year - 1) / 400

这就是“4 年一闰,100 年不闰,400 年又闰”的公式。

我们知道 1582-10-15 对应换历后的第一天,因此:

text
target = ordinal(1582-10-15) + (r - first_gregorian_day)

然后在格里高利历中找 target 对应的年月日。

如何从天数反推年月日

无论是儒略历还是格里高利历,都可以:

  1. 二分找到最大的 year,满足这一年的 1 月 1 日 不晚于目标天数;
  2. 用剩余天数从 1 月扫到 12 月;
  3. 得到日。

月份只有 12 个,所以直接扫月即可。

关键边界

这几个日期最容易错:

编号含义 日期
r = 0 1 1 4713 BC
1582-10-04 旧历最后一天
1582-10-15 新历第一天

中间的:

text
1582-10-05 到 1582-10-14

不存在,不能被输出。

代码

cpp
// 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

  • 每次询问二分年份,时间复杂度 O(logY)O(log Y),其中 Y 是答案年份范围;
  • 扫月份是常数 12
  • 总时间复杂度:O(QlogY)O(Q log Y)
  • 空间复杂度:O(1)O(1)

总结

这题不是普通日期模拟,真正的坑有三个:

  1. 公元前没有 0 年,内部最好改成连续的天文年份;
  2. 1582-10-04 后直接跳到 1582-10-15
  3. 换历前后闰年规则不同。

把时间轴按换历点分成两段后,问题就变成:

text
天数编号 -> 二分年份 -> 扫月份 -> 输出日期

本文的 main.cpp 已用逐日模拟版 brute.cpp 进行 300 组随机小范围儒略日对拍。