题解 儒略日
题解 儒略日
这种题目无论出到那场比赛都会被喷的惨的,这说明出题人早已做好了心理准备,大家快喷他。
题目分析
\(T\le 10^5\) 显然出题人是想放一个 \(\log_2\) 过的,所以我们算年份的时候可以使用二分。
大力分类讨论,我考场上分了这些时间段:
- 公元前 4713 年 1 月 1 日(含) 至 公元前 1 年 12 月 31 日(含)
- 公元 1 年 1 月 1 日(含) 至 公元 1582 年 10 月 4 日(含)
- 公元 1582 年 10 月 15 日(含) 至 公元 1582 年 12 月 31 日(含)
- 公元 1583 年 1 月 1 日(含) 至 公元 1000000000 年 12 月 31 日(含)
第一步找到这些时间段之间的断点:
const int data0[13]={0,31,28,31,30,31,30,31,31,30,31,30,31},//平年
data1[13]={0,31,29,31,30,31,30,31,31,30,31,30,31};//闰年
void getyear(void){//这个是用来获取平年一年天数的,也就是 365 天
int tmp=0;
for(int i=1;i<=12;++i)
tmp+=data0[i];
printf("year:%d\n",tmp);
}
void getsum(void){
int sum=0;
for(int i=4713;i>=1;--i)
sum+=365+(i%4==1);
//reach 1.1.1
printf("reach 1.1.1:%d\n",sum);
for(int i=1;i<=1581;++i)
sum+=365+(i%4==0);
//reach 1582.1.1
for(int i=1;i<=9;++i)
sum+=data0[i];
//reach 1582.10.1
sum+=4;
//reach 1582.10.5 1582.10.15
printf("reach 1582.10.15:%d\n",sum);
sum+=17;
//reach 1582.11.1
for(int i=11;i<=12;++i)
sum+=data0[i];
//reach 1583.1.1
printf("reach 1583.1.1:%d\n",sum);
}
执行 getsum()
函数,可以得到如下结果:
reach 1.1.1:1721424
reach 1582.10.15:2299161
reach 1583.1.1:2299239
于是我们可以得到,四个时间段儒略日的取值范围分别是 \([0,1721424),[1721424,2299161),[2299161,2299239),[2299239,+\infty)\) :
const int R0=1721424,R1=2299161,R2=2299239;
然后对每个时间段分别考虑。
公元前 4713 年 1 月 1 日(含) 至 公元前 1 年 12 月 31 日(含)
二分年份 \(Y\) ,考虑要从 公元前 4713 年 1 月 1 日 到 公元前 4713+\(Y\) 年 1 月 1 日 一共经过了多少天。
先列出表格表示每一年有多少天(不妨用负数表示公元前):
年份 | 天数 |
---|---|
-4713+0 | 366 |
-4713+1 | 365 |
-4713+2 | 365 |
-4713+3 | 365 |
-4713+4 | 366 |
-4713+5 | 365 |
\(\dots\) | \(\dots\) |
求出到每一年 1 月 1 日需要的天数:
年份 | 天数 |
---|---|
-4713+0 | \(0\times 365+0\) |
-4713+1 | \(1\times 365+1\) |
-4713+2 | \(2\times 365+1\) |
-4713+3 | \(3\times 365+1\) |
-4713+4 | \(4\times 365+1\) |
-4713+5 | \(5\times 365+2\) |
\(\dots\) | \(\dots\) |
可以得到,从 公元前 4713 年 1 月 1 日 到 公元前 4713+\(Y\) 年 1 月 1 日 经过的天数为:
于是就可以通过二分来求出 \(Y\) 了:
long long getyear0(long long r){
// r 表示从公元前 4713 年 1 月 1 日开始还需要经过多少天
long long L=0,R=4712;
while(L<R){
long long M=(L+R+1)>>1;
long long D=(M+3)/4+M*365;
if(D>r)R=M-1;else L=M;
}
return L;
}
得出 \(Y\) 之后,我们就可以直接枚举月份了,于是可以写出如下代码(需要注意一点,就是我代码中的 write()
表示输出, pc(x)
等同于 putchar(x)
):
if(r<R0){
//before 1.1.1
long long Y=getyear0(r);
r-=(Y+3)/4+Y*365;++r;
//这里 ++r 是因为此时已经是 1 月 1 日了,而后面 r 还需要用来计算天数,所以 r 初始就是 1
Y=4713-Y;long long M=1;
// Y 是年份, M 是月份
if(Y%4!=1){//平年
while(r>data0[M])
r-=data0[M],++M;
}
else{//闰年
while(r>data1[M])
r-=data1[M],++M;
}
write(r),pc(' '),write(M),pc(' '),write(Y),pc(' ');
pc('B'),pc('C');
}
公元 1 年 1 月 1 日(含) 至 公元 1582 年 10 月 4 日(含)
二分年份 \(Y\) ,考虑要从 公元 1 年 1 月 1 日 到 公元 1+\(Y\) 年 1 月 1 日 一共经过了多少天。
先列出表格表示每一年有多少天:
年份 | 天数 |
---|---|
1+0 | 365 |
1+1 | 365 |
1+2 | 365 |
1+3 | 366 |
1+4 | 365 |
1+5 | 365 |
\(\dots\) | \(\dots\) |
求出到每一年 1 月 1 日需要的天数:
年份 | 天数 |
---|---|
1+0 | \(0\times 365+0\) |
1+1 | \(1\times 365+0\) |
1+2 | \(2\times 365+0\) |
1+3 | \(3\times 365+0\) |
1+4 | \(4\times 365+1\) |
1+5 | \(5\times 365+1\) |
\(\dots\) | \(\dots\) |
可以得到,从 公元 1 年 1 月 1 日 到 公元 1+\(Y\) 年 1 月 1 日 经过的天数为:
于是就可以通过二分来求出 \(Y\) 了:
long long getyear1(long long r){
// r 表示从公元 1 年 1 月 1 日开始还需要经过多少天
long long L=0,R=1581;
while(L<R){
long long M=(L+R+1)>>1;
long long D=M/4+M*365;
if(D>r)R=M-1;else L=M;
}
return L;
}
得出 \(Y\) 之后,我们就可以直接枚举月份了,于是可以写出如下代码:
if(r>=R0&&r<R1){
//before 1582.10.5 1582.10.15
r-=R0;
long long Y=getyear1(r);
r-=Y/4+Y*365;++r;
//这里 ++r 是因为此时已经是 1 月 1 日了,而后面 r 还需要用来计算天数,所以 r 初始就是 1
Y=Y+1;long long M=1;
// Y 是年份, M 是月份
if(Y%4!=0){//平年
while(r>data0[M])
r-=data0[M],++M;
}
else{//闰年
while(r>data1[M])
r-=data1[M],++M;
}
write(r),pc(' '),write(M),pc(' '),write(Y);
}
公元 1582 年 10 月 15 日(含) 至 公元 1582 年 12 月 31 日(含)
10 月有 31 天,所以 10 月 15 日到 11 月 1 日需要经过 17 天,如果还需要经过的天数不足 17 天,就说明月份是 10 月,否则就枚举月份:
if(r>=R1&&r<R2){
//before 1583.1.1
r-=R1;
if(r<17){
r+=15;//从 10 月 15 日开始算起
write(r),pc(' '),write(10),pc(' '),write(1582);
}
else{
r-=17;++r;
//这里 ++r 是因为此时已经是 1582 年 11 月 1 日了,而后面 r 还需要用来计算天数,所以 r 初始就是 1
long long M=11;
// M 是月份
while(r>data0[M])
r-=data0[M],++M;
write(r),pc(' '),write(M),pc(' '),write(1582);
}
}
公元 1583 年 1 月 1 日(含) 至 公元 1000000000 年 12 月 31 日(含)
二分年份 \(Y\) ,考虑要从 公元 1583 年 1 月 1 日 到 公元 1583+\(Y\) 年 1 月 1 日 一共经过了多少天。
这里计算的时候肯定就不能仅仅 \(\div 4\) 再向下取整了,因为如果一个年份是 100 的倍数还必须是 400 的倍数才是闰年。
首先不考虑“一个年份是 100 的倍数还必须是 400 的倍数才是闰年”这个限制条件,由于 \(4\mid (1583+1),4\mid (1+3)\) ,此时需要经过的天数是:
再考虑把所有年份是 100 的倍数的年份额外造成的贡献减掉,由于 \(100\mid (1583+17),100\mid (17+83)\) ,此时需要经过的天数是:
再考虑把所有年份是 400 的倍数的年份造成的贡献加上,由于 \(400\mid (1583+17),400\mid (17+383)\) ,此时需要经过的天数就是:
于是就可以通过二分来求出 \(Y\) 了:
long long getyear2(long long r){
// r 表示从公元 1583 年 1 月 1 日开始还需要经过多少天
long long L=0,R=1000000000;
while(L<R){
long long M=(L+R+1)>>1;
long long D=(M+2)/4-(M+82)/100+(M+382)/400+M*365;
if(D>r)R=M-1;else L=M;
}
return L;
}
得出 \(Y\) 之后,我们就可以直接枚举月份了,于是可以写出如下代码:
if(r>=R2){
r-=R2;
long long Y=getyear2(r);
r-=(Y+2)/4-(Y+82)/100+(Y+382)/400+Y*365;++r;
//这里 ++r 是因为此时已经是 1583 年 1 月 1 日了,而后面 r 还需要用来计算天数,所以 r 初始就是 1
Y+=1583;long long M=1;
// Y 是年份, M 是月份
if(Y%4!=0||(Y%100==0&&Y%400!=0)){
while(r>data0[M])
r-=data0[M],++M;
}
else{
while(r>data1[M])
r-=data1[M],++M;
}
write(r),pc(' '),write(M),pc(' '),write(Y);
}
参考代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define ch() getchar()
#define pc(x) putchar(x)
using namespace std;
template<typename T>void read(T&x){
static int f;static char c;
for(c=ch(),f=1;c<'0'||c>'9';c=ch())if(c=='-')f=-f;
for(x=0;c>='0'&&c<='9';c=ch())x=x*10+(c&15);x*=f;
}
template<typename T>void write(T x){
static int q[64];int cnt=0;
if(x==0)return pc('0'),void();
if(x<0)pc('-'),x=-x;
while(x)q[cnt++]=x%10,x/=10;
while(cnt--)pc(q[cnt]+'0');
}
const int data0[13]={0,31,28,31,30,31,30,31,31,30,31,30,31},//平年
data1[13]={0,31,29,31,30,31,30,31,31,30,31,30,31};//闰年
void getyear(void){//这个是用来获取平年一年天数的,也就是 365 天
int tmp=0;
for(int i=1;i<=12;++i)
tmp+=data0[i];
printf("year:%d\n",tmp);
}
void getsum(void){
int sum=0;
for(int i=4713;i>=1;--i)
sum+=365+(i%4==1);
//reach 1.1.1
printf("reach 1.1.1:%d\n",sum);
for(int i=1;i<=1581;++i)
sum+=365+(i%4==0);
//reach 1582.1.1
for(int i=1;i<=9;++i)
sum+=data0[i];
//reach 1582.10.1
sum+=4;
//reach 1582.10.5 1582.10.15
printf("reach 1582.10.15:%d\n",sum);
sum+=17;
//reach 1582.11.1
for(int i=11;i<=12;++i)
sum+=data0[i];
//reach 1583.1.1
printf("reach 1583.1.1:%d\n",sum);
}
const int R0=1721424,R1=2299161,R2=2299239;
long long getyear0(long long r){
// r 表示从公元前 4713 年 1 月 1 日开始还需要经过多少天
long long L=0,R=4712;
while(L<R){
long long M=(L+R+1)>>1;
long long D=(M+3)/4+M*365;
if(D>r)R=M-1;else L=M;
}
return L;
}
long long getyear1(long long r){
// r 表示从公元 1 年 1 月 1 日开始还需要经过多少天
long long L=0,R=1581;
while(L<R){
long long M=(L+R+1)>>1;
long long D=M/4+M*365;
if(D>r)R=M-1;else L=M;
}
return L;
}
long long getyear2(long long r){
// r 表示从公元 1583 年 1 月 1 日开始还需要经过多少天
long long L=0,R=1000000000;
while(L<R){
long long M=(L+R+1)>>1;
long long D=(M+2)/4-(M+82)/100+(M+382)/400+M*365;
if(D>r)R=M-1;else L=M;
}
return L;
}
void work(void){
int q;read(q);
while(q--){
long long r;read(r);
if(r<R0){
//before 1.1.1
long long Y=getyear0(r);
r-=(Y+3)/4+Y*365;++r;
//这里 ++r 是因为此时已经是 1 月 1 日了,而后面 r 还需要用来计算天数,所以 r 初始就是 1
Y=4713-Y;long long M=1;
// Y 是年份, M 是月份
if(Y%4!=1){//平年
while(r>data0[M])
r-=data0[M],++M;
}
else{//闰年
while(r>data1[M])
r-=data1[M],++M;
}
write(r),pc(' '),write(M),pc(' '),write(Y),pc(' ');
pc('B'),pc('C');
}
else if(r>=R0&&r<R1){
//before 1582.10.5 1582.10.15
r-=R0;
long long Y=getyear1(r);
r-=Y/4+Y*365;++r;
//这里 ++r 是因为此时已经是 1 月 1 日了,而后面 r 还需要用来计算天数,所以 r 初始就是 1
Y=Y+1;long long M=1;
// Y 是年份, M 是月份
if(Y%4!=0){//平年
while(r>data0[M])
r-=data0[M],++M;
}
else{//闰年
while(r>data1[M])
r-=data1[M],++M;
}
write(r),pc(' '),write(M),pc(' '),write(Y);
}
else if(r>=R1&&r<R2){
//before 1583.1.1
r-=R1;
if(r<17){
r+=15;//从 10 月 15 日开始算起
write(r),pc(' '),write(10),pc(' '),write(1582);
}
else{
r-=17;++r;
//这里 ++r 是因为此时已经是 1582 年 11 月 1 日了,而后面 r 还需要用来计算天数,所以 r 初始就是 1
long long M=11;
// M 是月份
while(r>data0[M])
r-=data0[M],++M;
write(r),pc(' '),write(M),pc(' '),write(1582);
}
}
else if(r>=R2){
r-=R2;
long long Y=getyear2(r);
r-=(Y+2)/4-(Y+82)/100+(Y+382)/400+Y*365;++r;
//这里 ++r 是因为此时已经是 1583 年 1 月 1 日了,而后面 r 还需要用来计算天数,所以 r 初始就是 1
Y+=1583;long long M=1;
// Y 是年份, M 是月份
if(Y%4!=0||(Y%100==0&&Y%400!=0)){
while(r>data0[M])
r-=data0[M],++M;
}
else{
while(r>data1[M])
r-=data1[M],++M;
}
write(r),pc(' '),write(M),pc(' '),write(Y);
}
pc('\n');
}
}
int main(){
// getsum();
work();
return 0;
}